From 3bd3f2dff843e800791be6ac2ac570fc5b7d9b1a Mon Sep 17 00:00:00 2001 From: Aditya Wasan Date: Sun, 18 Oct 2020 12:19:21 +0530 Subject: [PATCH 01/13] Add saved entity and dao Signed-off-by: Aditya Wasan --- .../data/model/SavedLobstersEntity.kt | 14 +++++++ .../lobsters/data/source/SavedPostsDao.kt | 40 +++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 data/src/main/java/dev/msfjarvis/lobsters/data/model/SavedLobstersEntity.kt create mode 100644 data/src/main/java/dev/msfjarvis/lobsters/data/source/SavedPostsDao.kt diff --git a/data/src/main/java/dev/msfjarvis/lobsters/data/model/SavedLobstersEntity.kt b/data/src/main/java/dev/msfjarvis/lobsters/data/model/SavedLobstersEntity.kt new file mode 100644 index 00000000..909cb6f3 --- /dev/null +++ b/data/src/main/java/dev/msfjarvis/lobsters/data/model/SavedLobstersEntity.kt @@ -0,0 +1,14 @@ +package dev.msfjarvis.lobsters.data.model + +import androidx.room.Embedded +import androidx.room.Entity +import dev.msfjarvis.lobsters.model.LobstersPost + +@Entity( + tableName = "lobsters_saved_posts", + primaryKeys = ["shortId"], +) +data class SavedLobstersEntity( + @Embedded + val post: LobstersPost +) diff --git a/data/src/main/java/dev/msfjarvis/lobsters/data/source/SavedPostsDao.kt b/data/src/main/java/dev/msfjarvis/lobsters/data/source/SavedPostsDao.kt new file mode 100644 index 00000000..3fbc9ecf --- /dev/null +++ b/data/src/main/java/dev/msfjarvis/lobsters/data/source/SavedPostsDao.kt @@ -0,0 +1,40 @@ +package dev.msfjarvis.lobsters.data.source + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Transaction +import dev.msfjarvis.lobsters.data.model.LobstersEntity +import dev.msfjarvis.lobsters.data.model.SavedLobstersEntity +import dev.msfjarvis.lobsters.model.LobstersPost +import kotlinx.coroutines.flow.Flow + +@Dao +abstract class SavedPostsDao { + @Query("SELECT * FROM lobsters_saved_posts") + abstract fun loadPosts(): Flow> + + @Transaction + open suspend fun insertPosts(vararg posts: LobstersPost) { + insertPosts(posts.map { SavedLobstersEntity(it) }) + } + + @Insert(onConflict = OnConflictStrategy.IGNORE) + protected abstract suspend fun insertPosts(posts: List) + + @Transaction + open suspend fun deletePosts(vararg posts: LobstersPost) { + deletePosts(posts.map { SavedLobstersEntity(it) }) + } + + @Delete + protected abstract suspend fun deletePosts(posts: List) + + @Query("DELETE FROM lobsters_saved_posts") + abstract suspend fun deleteAllPosts() + + @Query("DELETE FROM lobsters_saved_posts WHERE shortId LIKE :shortId") + abstract suspend fun deletePostById(shortId: String) +} From d6d82248a8ffb6cadd4c487de97b0ac84cb1753b Mon Sep 17 00:00:00 2001 From: Aditya Wasan Date: Sun, 18 Oct 2020 16:03:07 +0530 Subject: [PATCH 02/13] Add saved lists feature Signed-off-by: Aditya Wasan --- .../dev/msfjarvis/lobsters/MainActivity.kt | 147 ++++++++++++++---- .../lobsters/data/LobstersViewModel.kt | 31 +++- .../dev/msfjarvis/lobsters/ui/LobstersItem.kt | 24 ++- .../java/dev/msfjarvis/lobsters/ui/Theme.kt | 3 + app/src/main/res/values/strings.xml | 1 + .../lobsters/data/source/PostsDao.kt | 10 ++ .../lobsters/data/source/PostsDatabase.kt | 5 +- .../msfjarvis/lobsters/model/LobstersPost.kt | 3 +- 8 files changed, 185 insertions(+), 39 deletions(-) diff --git a/app/src/main/java/dev/msfjarvis/lobsters/MainActivity.kt b/app/src/main/java/dev/msfjarvis/lobsters/MainActivity.kt index 0e91e2a1..b2c1865e 100644 --- a/app/src/main/java/dev/msfjarvis/lobsters/MainActivity.kt +++ b/app/src/main/java/dev/msfjarvis/lobsters/MainActivity.kt @@ -3,18 +3,34 @@ package dev.msfjarvis.lobsters import android.os.Bundle import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity +import androidx.compose.animation.animate +import androidx.compose.foundation.Icon import androidx.compose.foundation.Text import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumnForIndexed +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.FloatingActionButton +import androidx.compose.material.IconToggleButton +import androidx.compose.material.MaterialTheme import androidx.compose.material.Scaffold +import androidx.compose.material.TopAppBar +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Favorite +import androidx.compose.material.icons.filled.FavoriteBorder import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState import androidx.compose.runtime.Providers +import androidx.compose.runtime.State import androidx.compose.runtime.ambientOf import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.setContent @@ -23,8 +39,10 @@ import androidx.compose.ui.unit.dp import dagger.hilt.android.AndroidEntryPoint import dev.msfjarvis.lobsters.compose.utils.IconResource import dev.msfjarvis.lobsters.data.LobstersViewModel +import dev.msfjarvis.lobsters.model.LobstersPost import dev.msfjarvis.lobsters.ui.LobstersItem import dev.msfjarvis.lobsters.ui.LobstersTheme +import dev.msfjarvis.lobsters.ui.savedTitleColor import dev.msfjarvis.lobsters.urllauncher.UrlLauncher import javax.inject.Inject @@ -52,40 +70,111 @@ fun LobstersApp( viewModel: LobstersViewModel ) { val urlLauncher = UrlLauncherAmbient.current - val state = viewModel.posts.collectAsState() - val lastIndex = state.value.lastIndex + val hottestPostsState = viewModel.posts.collectAsState() + val savedPostsState = viewModel.savedPosts.collectAsState() + val lastIndex = hottestPostsState.value.lastIndex + val showSaved = remember { mutableStateOf(false) } Scaffold( + topBar = { LobsterTopAppbar(showSaved) }, bodyContent = { - if (state.value.isEmpty()) { - Column( - modifier = Modifier.fillMaxSize(), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - IconResource(R.drawable.ic_sync_problem_24px) - Text(stringResource(R.string.loading)) - } + if ((!showSaved.value && hottestPostsState.value.isEmpty()) || (showSaved.value && savedPostsState.value.isEmpty())) { + LobsterEmptyList(showSaved) } else { - LazyColumnForIndexed( - items = state.value, - modifier = Modifier.padding(horizontal = 8.dp) - ) { index, item -> - if (lastIndex == index) { - viewModel.getMorePosts() - } - LobstersItem( - item, - linkOpenAction = { post -> urlLauncher.launch(post.url.ifEmpty { post.commentsUrl }) }, - commentOpenAction = { post -> urlLauncher.launch(post.commentsUrl) }, - ) - } - } - }, - floatingActionButton = { - FloatingActionButton(onClick = { viewModel.refreshPosts() }) { - IconResource(resourceId = R.drawable.ic_refresh_24px) + LobsterList( + showSaved, + savedPostsState, + hottestPostsState, + lastIndex, + viewModel, + urlLauncher + ) } }, + floatingActionButton = { LobsterFAB(showSaved, viewModel) }, ) } + +@Composable +private fun LobsterFAB( + showSaved: MutableState, + viewModel: LobstersViewModel +) { + val animatedYOffset = animate(if (showSaved.value) 100.dp else 0.dp) + + FloatingActionButton( + onClick = { viewModel.refreshPosts() }, + modifier = Modifier.offset(y = animatedYOffset) + ) { + IconResource(resourceId = R.drawable.ic_refresh_24px) + } +} + +@Composable +private fun LobsterList( + showSaved: MutableState, + savedPostsState: State>, + hottestPostsState: State>, + lastIndex: Int, + viewModel: LobstersViewModel, + urlLauncher: UrlLauncher +) { + val hottestPostsListState = rememberLazyListState() + val savedPostsListState = rememberLazyListState() + + LazyColumnForIndexed( + items = if (showSaved.value) savedPostsState.value else hottestPostsState.value, + state = if (showSaved.value) savedPostsListState else hottestPostsListState, + modifier = Modifier.padding(horizontal = 8.dp) + ) { index, item -> + if (lastIndex == index && !showSaved.value) { + viewModel.getMorePosts() + } + LobstersItem( + item, + linkOpenAction = { post -> urlLauncher.launch(post.url.ifEmpty { post.commentsUrl }) }, + commentOpenAction = { post -> urlLauncher.launch(post.commentsUrl) }, + saveAction = { post -> if (!showSaved.value) viewModel.savePost(post) }, + ) + } +} + +@Composable +private fun LobsterEmptyList(showSaved: MutableState) { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + if (!showSaved.value) { + IconResource(R.drawable.ic_sync_problem_24px, modifier = Modifier.padding(16.dp)) + Text(stringResource(R.string.loading)) + } else { + Icon(Icons.Default.FavoriteBorder, tint = savedTitleColor, modifier = Modifier.padding(16.dp)) + Text(stringResource(R.string.no_saved_posts)) + } + } +} + +@Composable +private fun LobsterTopAppbar(showSaved: MutableState) { + TopAppBar { + Box(modifier = Modifier.fillMaxWidth()) { + Text( + text = if (showSaved.value) "Saved" else "Home", + modifier = Modifier.padding(16.dp).align(Alignment.CenterStart), + style = MaterialTheme.typography.h6, + ) + IconToggleButton( + checked = showSaved.value, + onCheckedChange = { showSaved.value = !showSaved.value }, + modifier = Modifier.padding(8.dp).align(Alignment.CenterEnd), + ) { + Icon( + asset = if (showSaved.value) Icons.Default.Favorite else Icons.Default.FavoriteBorder, + tint = savedTitleColor, + ) + } + } + } +} diff --git a/app/src/main/java/dev/msfjarvis/lobsters/data/LobstersViewModel.kt b/app/src/main/java/dev/msfjarvis/lobsters/data/LobstersViewModel.kt index b6b60a5c..8eb710a8 100644 --- a/app/src/main/java/dev/msfjarvis/lobsters/data/LobstersViewModel.kt +++ b/app/src/main/java/dev/msfjarvis/lobsters/data/LobstersViewModel.kt @@ -10,6 +10,7 @@ import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import java.net.SocketTimeoutException import java.net.UnknownHostException @@ -20,14 +21,16 @@ class LobstersViewModel @ViewModelInject constructor( ) : ViewModel() { private var apiPage = 1 private val _posts = MutableStateFlow>(emptyList()) - private val dao = database.postsDao() + private val _savedPosts = MutableStateFlow>(emptyList()) + private val postsDao = database.postsDao() + private val savedPostsDao = database.savedPostsDao() private val coroutineExceptionHandler = CoroutineExceptionHandler { _, throwable -> when (throwable) { // Swallow known network errors that can be recovered from. is UnknownHostException, is SocketTimeoutException -> { if (_posts.value.isEmpty()) { viewModelScope.launch { - dao.loadPosts().collectLatest { _posts.value = it } + postsDao.loadPosts().collectLatest { _posts.value = it } } } } @@ -35,9 +38,17 @@ class LobstersViewModel @ViewModelInject constructor( } } val posts: StateFlow> get() = _posts + val savedPosts: StateFlow> get() = _savedPosts init { getMorePostsInternal(true) + getSavedPosts() + } + + private fun getSavedPosts() { + viewModelScope.launch { + savedPostsDao.loadPosts().collectLatest { _savedPosts.value = it } + } } fun getMorePosts() { @@ -54,12 +65,24 @@ class LobstersViewModel @ViewModelInject constructor( val newPosts = lobstersApi.getHottestPosts(apiPage) if (firstLoad) { _posts.value = newPosts - dao.deleteAllPosts() + postsDao.deleteAllPosts() } else { _posts.value += newPosts } apiPage += 1 - dao.insertPosts(*_posts.value.toTypedArray()) + postsDao.insertPosts(*_posts.value.toTypedArray()) + } + } + + fun savePost(post: LobstersPost) { + viewModelScope.launch { + val tempList = _posts.value + _posts.value = tempList.map { + if (it.shortId == post.shortId) it.isLiked = true + it + } + savedPostsDao.insertPosts(post) + getSavedPosts() } } } diff --git a/app/src/main/java/dev/msfjarvis/lobsters/ui/LobstersItem.kt b/app/src/main/java/dev/msfjarvis/lobsters/ui/LobstersItem.kt index fc61b9d4..543edf94 100644 --- a/app/src/main/java/dev/msfjarvis/lobsters/ui/LobstersItem.kt +++ b/app/src/main/java/dev/msfjarvis/lobsters/ui/LobstersItem.kt @@ -1,5 +1,6 @@ package dev.msfjarvis.lobsters.ui +import androidx.compose.animation.animate import androidx.compose.foundation.Text import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -13,6 +14,8 @@ import androidx.compose.foundation.lazy.LazyColumnFor import androidx.compose.foundation.lazy.LazyItemScope import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -31,18 +34,27 @@ fun LazyItemScope.LobstersItem( modifier: Modifier = Modifier, linkOpenAction: (LobstersPost) -> Unit, commentOpenAction: (LobstersPost) -> Unit, + saveAction: (LobstersPost) -> Unit, ) { + val liked = remember { mutableStateOf(false) } + val titleColor = if (post.isLiked || liked.value) savedTitleColor else titleColor + Column( modifier = modifier .fillParentMaxWidth() .clickable( onClick = { linkOpenAction.invoke(post) }, - onLongClick = { commentOpenAction.invoke(post) } + onLongClick = { commentOpenAction.invoke(post) }, + onDoubleClick = { + post.isLiked = true + liked.value = true + saveAction.invoke(post) + }, ), ) { Text( text = post.title, - color = Color(0xFF7395D9), + color = titleColor, fontWeight = FontWeight.Bold, modifier = Modifier.padding(top = 4.dp) ) @@ -106,11 +118,15 @@ fun PreviewLobstersItem() { null, null, ), - listOf("openbsd") + listOf("openbsd"), ) LobstersTheme { LazyColumnFor(items = listOf(post)) { item -> - LobstersItem(post = item, linkOpenAction = {}, commentOpenAction = {}) + LobstersItem( + post = item, + linkOpenAction = {}, + commentOpenAction = {}, + saveAction = {}) } } } diff --git a/app/src/main/java/dev/msfjarvis/lobsters/ui/Theme.kt b/app/src/main/java/dev/msfjarvis/lobsters/ui/Theme.kt index c98c31f0..a2b3da07 100644 --- a/app/src/main/java/dev/msfjarvis/lobsters/ui/Theme.kt +++ b/app/src/main/java/dev/msfjarvis/lobsters/ui/Theme.kt @@ -5,6 +5,9 @@ import androidx.compose.material.darkColors import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color +val titleColor = Color(0xFF7395D9) +val savedTitleColor = Color(0xFFD97373) + val darkColors = darkColors( primary = Color.White, secondary = Color(0xFF6C0000), diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index df9597fb..3809cb8b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,4 +1,5 @@ lobste.rs Loading posts… + You don\'t have any saved posts diff --git a/data/src/main/java/dev/msfjarvis/lobsters/data/source/PostsDao.kt b/data/src/main/java/dev/msfjarvis/lobsters/data/source/PostsDao.kt index dcee27ce..1442710d 100644 --- a/data/src/main/java/dev/msfjarvis/lobsters/data/source/PostsDao.kt +++ b/data/src/main/java/dev/msfjarvis/lobsters/data/source/PostsDao.kt @@ -6,6 +6,7 @@ import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query import androidx.room.Transaction +import androidx.room.Update import dev.msfjarvis.lobsters.data.model.LobstersEntity import dev.msfjarvis.lobsters.model.LobstersPost import kotlinx.coroutines.flow.Flow @@ -15,6 +16,15 @@ abstract class PostsDao { @Query("SELECT * FROM lobsters_posts") abstract fun loadPosts(): Flow> + @Update + suspend fun updatePost(vararg posts: LobstersPost) { + updatePosts(posts.map { LobstersEntity(it) }) + } + + @Update(onConflict = OnConflictStrategy.IGNORE) + protected abstract suspend fun updatePosts(posts: List) + + @Transaction open suspend fun insertPosts(vararg posts: LobstersPost) { insertPosts(posts.map { LobstersEntity(it) }) diff --git a/data/src/main/java/dev/msfjarvis/lobsters/data/source/PostsDatabase.kt b/data/src/main/java/dev/msfjarvis/lobsters/data/source/PostsDatabase.kt index baaaeab1..d1e8a36e 100644 --- a/data/src/main/java/dev/msfjarvis/lobsters/data/source/PostsDatabase.kt +++ b/data/src/main/java/dev/msfjarvis/lobsters/data/source/PostsDatabase.kt @@ -4,12 +4,14 @@ import androidx.room.Database import androidx.room.RoomDatabase import androidx.room.TypeConverters import dev.msfjarvis.lobsters.data.model.LobstersEntity +import dev.msfjarvis.lobsters.data.model.SavedLobstersEntity @Database( entities = [ LobstersEntity::class, + SavedLobstersEntity::class ], - version = 1, + version = 2, exportSchema = false, ) @TypeConverters( @@ -17,4 +19,5 @@ import dev.msfjarvis.lobsters.data.model.LobstersEntity ) abstract class PostsDatabase : RoomDatabase() { abstract fun postsDao(): PostsDao + abstract fun savedPostsDao(): SavedPostsDao } diff --git a/model/src/main/java/dev/msfjarvis/lobsters/model/LobstersPost.kt b/model/src/main/java/dev/msfjarvis/lobsters/model/LobstersPost.kt index ed0496c0..5feb2f92 100644 --- a/model/src/main/java/dev/msfjarvis/lobsters/model/LobstersPost.kt +++ b/model/src/main/java/dev/msfjarvis/lobsters/model/LobstersPost.kt @@ -22,5 +22,6 @@ class LobstersPost( val commentsUrl: String, @Json(name = "submitter_user") val submitterUser: Submitter, - val tags: List + val tags: List, + var isLiked: Boolean = false, ) From 204e8d1b2ba5f3c44b6920b333d31cb136063732 Mon Sep 17 00:00:00 2001 From: Harsh Shandilya Date: Sun, 18 Oct 2020 22:47:51 +0530 Subject: [PATCH 03/13] Cleanup formatting Signed-off-by: Harsh Shandilya --- .../java/dev/msfjarvis/lobsters/compose/utils/DeferredIcon.kt | 2 +- .../main/java/dev/msfjarvis/lobsters/data/LobstersViewModel.kt | 1 - app/src/main/java/dev/msfjarvis/lobsters/di/ApiModule.kt | 1 + app/src/main/java/dev/msfjarvis/lobsters/ui/LobstersItem.kt | 1 - .../java/dev/msfjarvis/lobsters/data/source/SavedPostsDao.kt | 1 - .../src/test/java/dev/msfjarvis/lobsters/api/TestUtils.kt | 2 +- 6 files changed, 3 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/dev/msfjarvis/lobsters/compose/utils/DeferredIcon.kt b/app/src/main/java/dev/msfjarvis/lobsters/compose/utils/DeferredIcon.kt index c9301983..35116fc6 100644 --- a/app/src/main/java/dev/msfjarvis/lobsters/compose/utils/DeferredIcon.kt +++ b/app/src/main/java/dev/msfjarvis/lobsters/compose/utils/DeferredIcon.kt @@ -16,8 +16,8 @@ package dev.msfjarvis.lobsters.compose.utils import androidx.annotation.DrawableRes -import androidx.compose.foundation.Icon import androidx.compose.foundation.AmbientContentColor +import androidx.compose.foundation.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color diff --git a/app/src/main/java/dev/msfjarvis/lobsters/data/LobstersViewModel.kt b/app/src/main/java/dev/msfjarvis/lobsters/data/LobstersViewModel.kt index 8eb710a8..211aa69a 100644 --- a/app/src/main/java/dev/msfjarvis/lobsters/data/LobstersViewModel.kt +++ b/app/src/main/java/dev/msfjarvis/lobsters/data/LobstersViewModel.kt @@ -10,7 +10,6 @@ import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import java.net.SocketTimeoutException import java.net.UnknownHostException diff --git a/app/src/main/java/dev/msfjarvis/lobsters/di/ApiModule.kt b/app/src/main/java/dev/msfjarvis/lobsters/di/ApiModule.kt index dfa1042a..f7f970f4 100644 --- a/app/src/main/java/dev/msfjarvis/lobsters/di/ApiModule.kt +++ b/app/src/main/java/dev/msfjarvis/lobsters/di/ApiModule.kt @@ -11,6 +11,7 @@ import dev.msfjarvis.lobsters.api.LobstersApi @Module object ApiModule { const val LOBSTERS_URL = "https://lobste.rs" + @Provides fun provideLobstersApi(): LobstersApi { return ApiClient.getClient(LOBSTERS_URL) diff --git a/app/src/main/java/dev/msfjarvis/lobsters/ui/LobstersItem.kt b/app/src/main/java/dev/msfjarvis/lobsters/ui/LobstersItem.kt index 543edf94..7d831829 100644 --- a/app/src/main/java/dev/msfjarvis/lobsters/ui/LobstersItem.kt +++ b/app/src/main/java/dev/msfjarvis/lobsters/ui/LobstersItem.kt @@ -1,6 +1,5 @@ package dev.msfjarvis.lobsters.ui -import androidx.compose.animation.animate import androidx.compose.foundation.Text import androidx.compose.foundation.background import androidx.compose.foundation.clickable diff --git a/data/src/main/java/dev/msfjarvis/lobsters/data/source/SavedPostsDao.kt b/data/src/main/java/dev/msfjarvis/lobsters/data/source/SavedPostsDao.kt index 3fbc9ecf..80b9498b 100644 --- a/data/src/main/java/dev/msfjarvis/lobsters/data/source/SavedPostsDao.kt +++ b/data/src/main/java/dev/msfjarvis/lobsters/data/source/SavedPostsDao.kt @@ -6,7 +6,6 @@ import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query import androidx.room.Transaction -import dev.msfjarvis.lobsters.data.model.LobstersEntity import dev.msfjarvis.lobsters.data.model.SavedLobstersEntity import dev.msfjarvis.lobsters.model.LobstersPost import kotlinx.coroutines.flow.Flow diff --git a/lobsters-api/src/test/java/dev/msfjarvis/lobsters/api/TestUtils.kt b/lobsters-api/src/test/java/dev/msfjarvis/lobsters/api/TestUtils.kt index 04e358b7..7c856a9b 100644 --- a/lobsters-api/src/test/java/dev/msfjarvis/lobsters/api/TestUtils.kt +++ b/lobsters-api/src/test/java/dev/msfjarvis/lobsters/api/TestUtils.kt @@ -3,7 +3,7 @@ package dev.msfjarvis.lobsters.api import java.io.File object TestUtils { - fun getJson(path : String) : String { + fun getJson(path: String): String { // Load the JSON response val uri = javaClass.classLoader.getResource(path) val file = File(uri.path) From 10216048893846561ec83a280e216cdb2e699f84 Mon Sep 17 00:00:00 2001 From: Harsh Shandilya Date: Sun, 18 Oct 2020 22:58:51 +0530 Subject: [PATCH 04/13] data: add method to check if a post is already liked Signed-off-by: Harsh Shandilya --- .../java/dev/msfjarvis/lobsters/data/source/SavedPostsDao.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/data/src/main/java/dev/msfjarvis/lobsters/data/source/SavedPostsDao.kt b/data/src/main/java/dev/msfjarvis/lobsters/data/source/SavedPostsDao.kt index 80b9498b..bb51bbb6 100644 --- a/data/src/main/java/dev/msfjarvis/lobsters/data/source/SavedPostsDao.kt +++ b/data/src/main/java/dev/msfjarvis/lobsters/data/source/SavedPostsDao.kt @@ -36,4 +36,7 @@ abstract class SavedPostsDao { @Query("DELETE FROM lobsters_saved_posts WHERE shortId LIKE :shortId") abstract suspend fun deletePostById(shortId: String) + + @Query("SELECT EXISTS(SELECT 1 FROM lobsters_saved_posts WHERE shortId LIKE :shortId)") + abstract suspend fun isLiked(shortId: String): Boolean } From 0336af06243f717a640d96a510fb64c48555800b Mon Sep 17 00:00:00 2001 From: Harsh Shandilya Date: Sun, 18 Oct 2020 23:05:59 +0530 Subject: [PATCH 05/13] Set liked flag on first load Signed-off-by: Harsh Shandilya --- .../main/java/dev/msfjarvis/lobsters/data/LobstersViewModel.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/dev/msfjarvis/lobsters/data/LobstersViewModel.kt b/app/src/main/java/dev/msfjarvis/lobsters/data/LobstersViewModel.kt index 211aa69a..1a088743 100644 --- a/app/src/main/java/dev/msfjarvis/lobsters/data/LobstersViewModel.kt +++ b/app/src/main/java/dev/msfjarvis/lobsters/data/LobstersViewModel.kt @@ -62,6 +62,8 @@ class LobstersViewModel @ViewModelInject constructor( private fun getMorePostsInternal(firstLoad: Boolean) { viewModelScope.launch(coroutineExceptionHandler) { val newPosts = lobstersApi.getHottestPosts(apiPage) + .map { it.apply { if (savedPostsDao.isLiked(shortId)) isLiked = true } } + .toList() if (firstLoad) { _posts.value = newPosts postsDao.deleteAllPosts() From b4df9d87d88b7526e383703ba857172da4b31430 Mon Sep 17 00:00:00 2001 From: Harsh Shandilya Date: Sun, 18 Oct 2020 23:14:31 +0530 Subject: [PATCH 06/13] Allow reusing save action to remove from saved list Signed-off-by: Harsh Shandilya --- app/src/main/java/dev/msfjarvis/lobsters/MainActivity.kt | 8 +++++++- .../java/dev/msfjarvis/lobsters/data/LobstersViewModel.kt | 7 +++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/dev/msfjarvis/lobsters/MainActivity.kt b/app/src/main/java/dev/msfjarvis/lobsters/MainActivity.kt index b2c1865e..78607540 100644 --- a/app/src/main/java/dev/msfjarvis/lobsters/MainActivity.kt +++ b/app/src/main/java/dev/msfjarvis/lobsters/MainActivity.kt @@ -134,7 +134,13 @@ private fun LobsterList( item, linkOpenAction = { post -> urlLauncher.launch(post.url.ifEmpty { post.commentsUrl }) }, commentOpenAction = { post -> urlLauncher.launch(post.commentsUrl) }, - saveAction = { post -> if (!showSaved.value) viewModel.savePost(post) }, + saveAction = { post -> + if (showSaved.value) { + viewModel.removeSavedPost(post) + } else { + viewModel.savePost(post) + } + }, ) } } diff --git a/app/src/main/java/dev/msfjarvis/lobsters/data/LobstersViewModel.kt b/app/src/main/java/dev/msfjarvis/lobsters/data/LobstersViewModel.kt index 1a088743..3558cff7 100644 --- a/app/src/main/java/dev/msfjarvis/lobsters/data/LobstersViewModel.kt +++ b/app/src/main/java/dev/msfjarvis/lobsters/data/LobstersViewModel.kt @@ -86,4 +86,11 @@ class LobstersViewModel @ViewModelInject constructor( getSavedPosts() } } + + fun removeSavedPost(post: LobstersPost) { + viewModelScope.launch { + savedPostsDao.deletePostById(post.shortId) + getSavedPosts() + } + } } From e35a25b1351acacb1b78827040ed15dd7c97f0a5 Mon Sep 17 00:00:00 2001 From: Harsh Shandilya Date: Sun, 18 Oct 2020 23:26:28 +0530 Subject: [PATCH 07/13] Simplify API of child components Signed-off-by: Harsh Shandilya --- .../dev/msfjarvis/lobsters/MainActivity.kt | 83 ++++++++++--------- 1 file changed, 43 insertions(+), 40 deletions(-) diff --git a/app/src/main/java/dev/msfjarvis/lobsters/MainActivity.kt b/app/src/main/java/dev/msfjarvis/lobsters/MainActivity.kt index 78607540..badd3af8 100644 --- a/app/src/main/java/dev/msfjarvis/lobsters/MainActivity.kt +++ b/app/src/main/java/dev/msfjarvis/lobsters/MainActivity.kt @@ -3,7 +3,6 @@ package dev.msfjarvis.lobsters import android.os.Bundle import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity -import androidx.compose.animation.animate import androidx.compose.foundation.Icon import androidx.compose.foundation.Text import androidx.compose.foundation.layout.Arrangement @@ -11,7 +10,6 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumnForIndexed import androidx.compose.foundation.lazy.rememberLazyListState @@ -24,9 +22,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Favorite import androidx.compose.material.icons.filled.FavoriteBorder import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState import androidx.compose.runtime.Providers -import androidx.compose.runtime.State import androidx.compose.runtime.ambientOf import androidx.compose.runtime.collectAsState import androidx.compose.runtime.mutableStateOf @@ -70,51 +66,58 @@ fun LobstersApp( viewModel: LobstersViewModel ) { val urlLauncher = UrlLauncherAmbient.current - val hottestPostsState = viewModel.posts.collectAsState() - val savedPostsState = viewModel.savedPosts.collectAsState() - val lastIndex = hottestPostsState.value.lastIndex + val posts = viewModel.posts.collectAsState() + val savedPosts = viewModel.savedPosts.collectAsState() + val lastIndex = posts.value.lastIndex val showSaved = remember { mutableStateOf(false) } Scaffold( - topBar = { LobsterTopAppbar(showSaved) }, + topBar = { + LobstersTopAppBar(showSaved.value) { + showSaved.value = !showSaved.value + } + }, bodyContent = { - if ((!showSaved.value && hottestPostsState.value.isEmpty()) || (showSaved.value && savedPostsState.value.isEmpty())) { - LobsterEmptyList(showSaved) + val saved = showSaved.value + if (saved && savedPosts.value.isEmpty()) { + EmptyList(saved) + } else if (!saved && posts.value.isEmpty()) { + EmptyList(saved) } else { LobsterList( - showSaved, - savedPostsState, - hottestPostsState, + showSaved.value, + savedPosts.value, + posts.value, lastIndex, viewModel, urlLauncher ) } }, - floatingActionButton = { LobsterFAB(showSaved, viewModel) }, + floatingActionButton = { LobstersFAB(showSaved.value, viewModel) }, ) } @Composable -private fun LobsterFAB( - showSaved: MutableState, +private fun LobstersFAB( + showSaved: Boolean, viewModel: LobstersViewModel ) { - val animatedYOffset = animate(if (showSaved.value) 100.dp else 0.dp) - - FloatingActionButton( - onClick = { viewModel.refreshPosts() }, - modifier = Modifier.offset(y = animatedYOffset) - ) { - IconResource(resourceId = R.drawable.ic_refresh_24px) + if (!showSaved) { + FloatingActionButton( + onClick = { viewModel.refreshPosts() }, + modifier = Modifier + ) { + IconResource(resourceId = R.drawable.ic_refresh_24px) + } } } @Composable private fun LobsterList( - showSaved: MutableState, - savedPostsState: State>, - hottestPostsState: State>, + showSaved: Boolean, + savedPosts: List, + hottestPosts: List, lastIndex: Int, viewModel: LobstersViewModel, urlLauncher: UrlLauncher @@ -123,11 +126,11 @@ private fun LobsterList( val savedPostsListState = rememberLazyListState() LazyColumnForIndexed( - items = if (showSaved.value) savedPostsState.value else hottestPostsState.value, - state = if (showSaved.value) savedPostsListState else hottestPostsListState, + items = if (showSaved) savedPosts else hottestPosts, + state = if (showSaved) savedPostsListState else hottestPostsListState, modifier = Modifier.padding(horizontal = 8.dp) ) { index, item -> - if (lastIndex == index && !showSaved.value) { + if (lastIndex == index && !showSaved) { viewModel.getMorePosts() } LobstersItem( @@ -135,7 +138,7 @@ private fun LobsterList( linkOpenAction = { post -> urlLauncher.launch(post.url.ifEmpty { post.commentsUrl }) }, commentOpenAction = { post -> urlLauncher.launch(post.commentsUrl) }, saveAction = { post -> - if (showSaved.value) { + if (showSaved) { viewModel.removeSavedPost(post) } else { viewModel.savePost(post) @@ -146,38 +149,38 @@ private fun LobsterList( } @Composable -private fun LobsterEmptyList(showSaved: MutableState) { +private fun EmptyList(showSaved: Boolean) { Column( modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally, ) { - if (!showSaved.value) { - IconResource(R.drawable.ic_sync_problem_24px, modifier = Modifier.padding(16.dp)) - Text(stringResource(R.string.loading)) - } else { + if (showSaved) { Icon(Icons.Default.FavoriteBorder, tint = savedTitleColor, modifier = Modifier.padding(16.dp)) Text(stringResource(R.string.no_saved_posts)) + } else { + IconResource(R.drawable.ic_sync_problem_24px, modifier = Modifier.padding(16.dp)) + Text(stringResource(R.string.loading)) } } } @Composable -private fun LobsterTopAppbar(showSaved: MutableState) { +private fun LobstersTopAppBar(showSaved: Boolean, toggleAction: () -> Unit) { TopAppBar { Box(modifier = Modifier.fillMaxWidth()) { Text( - text = if (showSaved.value) "Saved" else "Home", + text = if (showSaved) "Saved" else "Home", modifier = Modifier.padding(16.dp).align(Alignment.CenterStart), style = MaterialTheme.typography.h6, ) IconToggleButton( - checked = showSaved.value, - onCheckedChange = { showSaved.value = !showSaved.value }, + checked = showSaved, + onCheckedChange = { toggleAction.invoke() }, modifier = Modifier.padding(8.dp).align(Alignment.CenterEnd), ) { Icon( - asset = if (showSaved.value) Icons.Default.Favorite else Icons.Default.FavoriteBorder, + asset = if (showSaved) Icons.Default.Favorite else Icons.Default.FavoriteBorder, tint = savedTitleColor, ) } From c54b375c06f3d332db3a64ac45b28a61924cb48b Mon Sep 17 00:00:00 2001 From: Harsh Shandilya Date: Sun, 18 Oct 2020 23:28:33 +0530 Subject: [PATCH 08/13] Liked flag toggle should go both ways Signed-off-by: Harsh Shandilya --- .../main/java/dev/msfjarvis/lobsters/data/LobstersViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/dev/msfjarvis/lobsters/data/LobstersViewModel.kt b/app/src/main/java/dev/msfjarvis/lobsters/data/LobstersViewModel.kt index 3558cff7..540dd557 100644 --- a/app/src/main/java/dev/msfjarvis/lobsters/data/LobstersViewModel.kt +++ b/app/src/main/java/dev/msfjarvis/lobsters/data/LobstersViewModel.kt @@ -62,7 +62,7 @@ class LobstersViewModel @ViewModelInject constructor( private fun getMorePostsInternal(firstLoad: Boolean) { viewModelScope.launch(coroutineExceptionHandler) { val newPosts = lobstersApi.getHottestPosts(apiPage) - .map { it.apply { if (savedPostsDao.isLiked(shortId)) isLiked = true } } + .map { it.apply { isLiked = savedPostsDao.isLiked(shortId) } } .toList() if (firstLoad) { _posts.value = newPosts From d93910ba5acb26ee10012256dc017bd8e02857d1 Mon Sep 17 00:00:00 2001 From: Aditya Wasan Date: Mon, 19 Oct 2020 00:09:45 +0530 Subject: [PATCH 09/13] Pass correct variable to EmptyList Signed-off-by: Aditya Wasan --- app/src/main/java/dev/msfjarvis/lobsters/MainActivity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/dev/msfjarvis/lobsters/MainActivity.kt b/app/src/main/java/dev/msfjarvis/lobsters/MainActivity.kt index badd3af8..2af3b0fa 100644 --- a/app/src/main/java/dev/msfjarvis/lobsters/MainActivity.kt +++ b/app/src/main/java/dev/msfjarvis/lobsters/MainActivity.kt @@ -82,7 +82,7 @@ fun LobstersApp( if (saved && savedPosts.value.isEmpty()) { EmptyList(saved) } else if (!saved && posts.value.isEmpty()) { - EmptyList(saved) + EmptyList(!saved) } else { LobsterList( showSaved.value, From dc83ad2deecb1c86ab21774baaa75ab957f85699 Mon Sep 17 00:00:00 2001 From: Harsh Shandilya Date: Mon, 19 Oct 2020 00:20:07 +0530 Subject: [PATCH 10/13] Update liked state when removing a saved post as well Signed-off-by: Harsh Shandilya --- .../java/dev/msfjarvis/lobsters/data/LobstersViewModel.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/dev/msfjarvis/lobsters/data/LobstersViewModel.kt b/app/src/main/java/dev/msfjarvis/lobsters/data/LobstersViewModel.kt index 540dd557..15c4b27a 100644 --- a/app/src/main/java/dev/msfjarvis/lobsters/data/LobstersViewModel.kt +++ b/app/src/main/java/dev/msfjarvis/lobsters/data/LobstersViewModel.kt @@ -62,7 +62,7 @@ class LobstersViewModel @ViewModelInject constructor( private fun getMorePostsInternal(firstLoad: Boolean) { viewModelScope.launch(coroutineExceptionHandler) { val newPosts = lobstersApi.getHottestPosts(apiPage) - .map { it.apply { isLiked = savedPostsDao.isLiked(shortId) } } + .transformLikedFlag() .toList() if (firstLoad) { _posts.value = newPosts @@ -91,6 +91,11 @@ class LobstersViewModel @ViewModelInject constructor( viewModelScope.launch { savedPostsDao.deletePostById(post.shortId) getSavedPosts() + _posts.value = _posts.value.transformLikedFlag().toList() } } + + private suspend fun List.transformLikedFlag() = map { + it.apply { isLiked = savedPostsDao.isLiked(shortId) } + } } From a38a52ebf36f7c604498c008c1ea7b9b454138e2 Mon Sep 17 00:00:00 2001 From: Aditya Wasan Date: Mon, 19 Oct 2020 00:24:30 +0530 Subject: [PATCH 11/13] Use transformLikedFlag in savePost method Signed-off-by: Aditya Wasan --- .../java/dev/msfjarvis/lobsters/data/LobstersViewModel.kt | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/src/main/java/dev/msfjarvis/lobsters/data/LobstersViewModel.kt b/app/src/main/java/dev/msfjarvis/lobsters/data/LobstersViewModel.kt index 15c4b27a..a01781f3 100644 --- a/app/src/main/java/dev/msfjarvis/lobsters/data/LobstersViewModel.kt +++ b/app/src/main/java/dev/msfjarvis/lobsters/data/LobstersViewModel.kt @@ -77,13 +77,9 @@ class LobstersViewModel @ViewModelInject constructor( fun savePost(post: LobstersPost) { viewModelScope.launch { - val tempList = _posts.value - _posts.value = tempList.map { - if (it.shortId == post.shortId) it.isLiked = true - it - } savedPostsDao.insertPosts(post) getSavedPosts() + _posts.value = _posts.value.transformLikedFlag().toList() } } From 600c93ed0f59c62d1ec00256cabced4d5f852fd6 Mon Sep 17 00:00:00 2001 From: Harsh Shandilya Date: Mon, 19 Oct 2020 00:24:46 +0530 Subject: [PATCH 12/13] Replace material icons with drawable resources Signed-off-by: Harsh Shandilya --- .../java/dev/msfjarvis/lobsters/MainActivity.kt | 14 +++++++------- app/src/main/res/drawable/ic_favorite_24px.xml | 9 +++++++++ .../main/res/drawable/ic_favorite_border_24px.xml | 9 +++++++++ 3 files changed, 25 insertions(+), 7 deletions(-) create mode 100644 app/src/main/res/drawable/ic_favorite_24px.xml create mode 100644 app/src/main/res/drawable/ic_favorite_border_24px.xml diff --git a/app/src/main/java/dev/msfjarvis/lobsters/MainActivity.kt b/app/src/main/java/dev/msfjarvis/lobsters/MainActivity.kt index 2af3b0fa..04ce7d70 100644 --- a/app/src/main/java/dev/msfjarvis/lobsters/MainActivity.kt +++ b/app/src/main/java/dev/msfjarvis/lobsters/MainActivity.kt @@ -3,7 +3,6 @@ package dev.msfjarvis.lobsters import android.os.Bundle import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity -import androidx.compose.foundation.Icon import androidx.compose.foundation.Text import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -18,9 +17,6 @@ import androidx.compose.material.IconToggleButton import androidx.compose.material.MaterialTheme import androidx.compose.material.Scaffold import androidx.compose.material.TopAppBar -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Favorite -import androidx.compose.material.icons.filled.FavoriteBorder import androidx.compose.runtime.Composable import androidx.compose.runtime.Providers import androidx.compose.runtime.ambientOf @@ -156,7 +152,11 @@ private fun EmptyList(showSaved: Boolean) { horizontalAlignment = Alignment.CenterHorizontally, ) { if (showSaved) { - Icon(Icons.Default.FavoriteBorder, tint = savedTitleColor, modifier = Modifier.padding(16.dp)) + IconResource( + R.drawable.ic_favorite_border_24px, + tint = savedTitleColor, + modifier = Modifier.padding(16.dp) + ) Text(stringResource(R.string.no_saved_posts)) } else { IconResource(R.drawable.ic_sync_problem_24px, modifier = Modifier.padding(16.dp)) @@ -179,8 +179,8 @@ private fun LobstersTopAppBar(showSaved: Boolean, toggleAction: () -> Unit) { onCheckedChange = { toggleAction.invoke() }, modifier = Modifier.padding(8.dp).align(Alignment.CenterEnd), ) { - Icon( - asset = if (showSaved) Icons.Default.Favorite else Icons.Default.FavoriteBorder, + IconResource( + resourceId = if (showSaved) R.drawable.ic_favorite_24px else R.drawable.ic_favorite_border_24px, tint = savedTitleColor, ) } diff --git a/app/src/main/res/drawable/ic_favorite_24px.xml b/app/src/main/res/drawable/ic_favorite_24px.xml new file mode 100644 index 00000000..ce351f43 --- /dev/null +++ b/app/src/main/res/drawable/ic_favorite_24px.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_favorite_border_24px.xml b/app/src/main/res/drawable/ic_favorite_border_24px.xml new file mode 100644 index 00000000..e6646709 --- /dev/null +++ b/app/src/main/res/drawable/ic_favorite_border_24px.xml @@ -0,0 +1,9 @@ + + + From 5782ae9438743a3e1575be112e90f6f15677b110 Mon Sep 17 00:00:00 2001 From: Harsh Shandilya Date: Mon, 19 Oct 2020 00:26:55 +0530 Subject: [PATCH 13/13] Revert "Pass correct variable to EmptyList" This is wrong and causes the saved posts message to be shown on both screens. This reverts commit d93910ba5acb26ee10012256dc017bd8e02857d1. --- app/src/main/java/dev/msfjarvis/lobsters/MainActivity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/dev/msfjarvis/lobsters/MainActivity.kt b/app/src/main/java/dev/msfjarvis/lobsters/MainActivity.kt index 04ce7d70..fedaf6be 100644 --- a/app/src/main/java/dev/msfjarvis/lobsters/MainActivity.kt +++ b/app/src/main/java/dev/msfjarvis/lobsters/MainActivity.kt @@ -78,7 +78,7 @@ fun LobstersApp( if (saved && savedPosts.value.isEmpty()) { EmptyList(saved) } else if (!saved && posts.value.isEmpty()) { - EmptyList(!saved) + EmptyList(saved) } else { LobsterList( showSaved.value,