From 1788f2b4a236f3d2d2bc412a3e61ab73cf358bb6 Mon Sep 17 00:00:00 2001 From: Caren Chang Date: Fri, 23 Sep 2022 15:12:20 -0700 Subject: [PATCH] Enable bookmarks on topics page --- .../nowinandroid/feature/topic/TopicScreen.kt | 55 ++++--- .../feature/topic/TopicViewModel.kt | 154 ++++++++++++------ 2 files changed, 144 insertions(+), 65 deletions(-) diff --git a/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt b/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt index 08cd7a8ad..be6efaf3e 100644 --- a/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt +++ b/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicScreen.kt @@ -52,6 +52,7 @@ import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaFilte import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaLoadingWheel import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic +import com.google.samples.apps.nowinandroid.core.model.data.SaveableNewsResource import com.google.samples.apps.nowinandroid.core.model.data.previewNewsResources import com.google.samples.apps.nowinandroid.core.model.data.previewTopics import com.google.samples.apps.nowinandroid.core.ui.newsResourceCardItems @@ -65,24 +66,27 @@ fun TopicRoute( modifier: Modifier = Modifier, viewModel: TopicViewModel = hiltViewModel(), ) { - val uiState: TopicScreenUiState by viewModel.uiState.collectAsStateWithLifecycle() + val topicUiState: TopicUiState by viewModel.topicUiState.collectAsStateWithLifecycle() + val newsUiState: NewsUiState by viewModel.newUiState.collectAsStateWithLifecycle() TopicScreen( - topicState = uiState.topicState, - newsState = uiState.newsState, + topicState = topicUiState, + newsUiState = newsUiState, modifier = modifier, onBackClick = onBackClick, onFollowClick = viewModel::followTopicToggle, - ) + onBookmarkChanged = viewModel::bookmarkNews, + ) } @VisibleForTesting @Composable internal fun TopicScreen( topicState: TopicUiState, - newsState: NewsUiState, + newsUiState: NewsUiState, onBackClick: () -> Unit, onFollowClick: (Boolean) -> Unit, + onBookmarkChanged: (String, Boolean) -> Unit, modifier: Modifier = Modifier, ) { LazyColumn( @@ -111,8 +115,9 @@ internal fun TopicScreen( TopicBody( name = topicState.followableTopic.topic.name, description = topicState.followableTopic.topic.longDescription, - news = newsState, - imageUrl = topicState.followableTopic.topic.imageUrl + news = newsUiState, + imageUrl = topicState.followableTopic.topic.imageUrl, + onBookmarkChanged = onBookmarkChanged ) } } @@ -126,14 +131,15 @@ private fun LazyListScope.TopicBody( name: String, description: String, news: NewsUiState, - imageUrl: String + imageUrl: String, + onBookmarkChanged: (String, Boolean) -> Unit ) { // TODO: Show icon if available item { TopicHeader(name, description, imageUrl) } - TopicCards(news) + TopicCards(news, onBookmarkChanged) } @Composable @@ -160,14 +166,17 @@ private fun TopicHeader(name: String, description: String, imageUrl: String) { } } -private fun LazyListScope.TopicCards(news: NewsUiState) { +private fun LazyListScope.TopicCards( + news: NewsUiState, + onBookmarkChanged: (String, Boolean) -> Unit +) { when (news) { is NewsUiState.Success -> { newsResourceCardItems( items = news.news, - newsResourceMapper = { it }, - isBookmarkedMapper = { /* TODO */ false }, - onToggleBookmark = { /* TODO */ }, + newsResourceMapper = { it.newsResource }, + isBookmarkedMapper = { it.isSaved }, + onToggleBookmark = { onBookmarkChanged(it.newsResource.id, !it.isSaved) }, itemModifier = Modifier.padding(24.dp) ) } @@ -187,7 +196,7 @@ private fun TopicBodyPreview() { LazyColumn { TopicBody( "Jetpack Compose", "Lorem ipsum maximum", - NewsUiState.Success(emptyList()), "" + NewsUiState.Success(emptyList()), "", { _, _ -> } ) } } @@ -238,9 +247,16 @@ fun TopicScreenPopulated() { NiaBackground { TopicScreen( topicState = TopicUiState.Success(FollowableTopic(previewTopics[0], false)), - newsState = NewsUiState.Success(previewNewsResources), - onBackClick = {}, - onFollowClick = {} + newsUiState = NewsUiState.Success( + previewNewsResources.mapIndexed { index, newsResource -> + SaveableNewsResource( + newsResource = newsResource, + isSaved = index % 2 == 0, + ) + } + ), onBackClick = {}, + onFollowClick = {}, + onBookmarkChanged = { _, _ -> }, ) } } @@ -256,9 +272,10 @@ fun TopicScreenLoading() { NiaBackground { TopicScreen( topicState = TopicUiState.Loading, - newsState = NewsUiState.Loading, + newsUiState = NewsUiState.Loading, onBackClick = {}, - onFollowClick = {} + onFollowClick = {}, + onBookmarkChanged = { _, _ -> }, ) } } diff --git a/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModel.kt b/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModel.kt index d647e4bf7..823e1ec45 100644 --- a/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModel.kt +++ b/feature/topic/src/main/java/com/google/samples/apps/nowinandroid/feature/topic/TopicViewModel.kt @@ -19,11 +19,15 @@ package com.google.samples.apps.nowinandroid.feature.topic import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.google.samples.apps.nowinandroid.core.data.repository.AuthorsRepository import com.google.samples.apps.nowinandroid.core.data.repository.NewsRepository import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository +import com.google.samples.apps.nowinandroid.core.model.data.Author +import com.google.samples.apps.nowinandroid.core.model.data.FollowableAuthor import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic import com.google.samples.apps.nowinandroid.core.model.data.NewsResource +import com.google.samples.apps.nowinandroid.core.model.data.SaveableNewsResource import com.google.samples.apps.nowinandroid.core.model.data.Topic import com.google.samples.apps.nowinandroid.core.result.Result import com.google.samples.apps.nowinandroid.core.result.asResult @@ -48,63 +52,126 @@ class TopicViewModel @Inject constructor( private val topicId: String = checkNotNull(savedStateHandle[TopicDestination.topicIdArg]) + val topicUiState: StateFlow = topicUiStateStream( + topicId = topicId, + userDataRepository = userDataRepository, + topicsRepository = topicsRepository + ) + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = TopicUiState.Loading + ) + + val newUiState: StateFlow = newsUiStateStream( + topicId = topicId, + userDataRepository = userDataRepository, + newsRepository = newsRepository + ) + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = NewsUiState.Loading + ) + + fun followTopicToggle(followed: Boolean) { + viewModelScope.launch { + userDataRepository.toggleFollowedTopicId(topicId, followed) + } + } + + fun bookmarkNews(newsResourceId: String, bookmarked: Boolean) { + viewModelScope.launch { + userDataRepository.updateNewsResourceBookmark(newsResourceId, bookmarked) + } + } +} + +private fun topicUiStateStream( + topicId: String, + userDataRepository: UserDataRepository, + topicsRepository: TopicsRepository, +): Flow { // Observe the followed topics, as they could change over time. - private val followedTopicIdsStream: Flow>> = + val followedTopicIdsStream: Flow> = userDataRepository.userDataStream .map { it.followedTopics } - .asResult() // Observe topic information - private val topic: Flow> = topicsRepository.getTopic(topicId).asResult() - - // Observe the News for this topic - private val newsStream: Flow>> = - newsRepository.getNewsResourcesStream( - filterTopicIds = setOf(element = topicId), - ).asResult() - - val uiState: StateFlow = - combine( - followedTopicIdsStream, - topic, - newsStream - ) { followedTopicsResult, topicResult, newsResult -> - val topic: TopicUiState = - if (topicResult is Result.Success && followedTopicsResult is Result.Success) { - val followed = followedTopicsResult.data.contains(topicId) + val topicStream: Flow = topicsRepository.getTopic( + id = topicId + ) + + return combine( + followedTopicIdsStream, + topicStream, + ::Pair + ) + .asResult() + .map { followedTopicToTopicResult -> + when (followedTopicToTopicResult) { + is Result.Success -> { + val (followedTopics, topic) = followedTopicToTopicResult.data + val followed = followedTopics.contains(topicId) TopicUiState.Success( followableTopic = FollowableTopic( - topic = topicResult.data, + topic = topic, isFollowed = followed ) ) - } else if ( - topicResult is Result.Loading || followedTopicsResult is Result.Loading - ) { + } + is Result.Loading -> { TopicUiState.Loading - } else { + } + is Result.Error -> { TopicUiState.Error } - - val news: NewsUiState = when (newsResult) { - is Result.Success -> NewsUiState.Success(newsResult.data) - is Result.Loading -> NewsUiState.Loading - is Result.Error -> NewsUiState.Error } - - TopicScreenUiState(topic, news) } - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000), - initialValue = TopicScreenUiState(TopicUiState.Loading, NewsUiState.Loading) - ) +} - fun followTopicToggle(followed: Boolean) { - viewModelScope.launch { - userDataRepository.toggleFollowedTopicId(topicId, followed) +private fun newsUiStateStream( + topicId: String, + newsRepository: NewsRepository, + userDataRepository: UserDataRepository, +): Flow { + // Observe news + val newsStream: Flow> = newsRepository.getNewsResourcesStream( + filterAuthorIds = emptySet(), + filterTopicIds = setOf(element = topicId), + ) + + // Observe bookmarks + val bookmarkStream: Flow> = userDataRepository.userDataStream + .map { it.bookmarkedNewsResources } + + return combine( + newsStream, + bookmarkStream, + ::Pair + ) + .asResult() + .map { newsToBookmarksResult -> + when (newsToBookmarksResult) { + is Result.Success -> { + val (news, bookmarks) = newsToBookmarksResult.data + NewsUiState.Success( + news.map { newsResource -> + SaveableNewsResource( + newsResource, + isSaved = bookmarks.contains(newsResource.id) + ) + } + ) + } + is Result.Loading -> { + NewsUiState.Loading + } + is Result.Error -> { + NewsUiState.Error + } + } } - } } sealed interface TopicUiState { @@ -114,12 +181,7 @@ sealed interface TopicUiState { } sealed interface NewsUiState { - data class Success(val news: List) : NewsUiState + data class Success(val news: List) : NewsUiState object Error : NewsUiState object Loading : NewsUiState } - -data class TopicScreenUiState( - val topicState: TopicUiState, - val newsState: NewsUiState -)