Enable bookmarks on topics page

pull/1837/head
Caren Chang 3 years ago
parent 437b9a6660
commit 1788f2b4a2

@ -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.component.NiaLoadingWheel
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme 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.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.previewNewsResources
import com.google.samples.apps.nowinandroid.core.model.data.previewTopics import com.google.samples.apps.nowinandroid.core.model.data.previewTopics
import com.google.samples.apps.nowinandroid.core.ui.newsResourceCardItems import com.google.samples.apps.nowinandroid.core.ui.newsResourceCardItems
@ -65,24 +66,27 @@ fun TopicRoute(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: TopicViewModel = hiltViewModel(), 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( TopicScreen(
topicState = uiState.topicState, topicState = topicUiState,
newsState = uiState.newsState, newsUiState = newsUiState,
modifier = modifier, modifier = modifier,
onBackClick = onBackClick, onBackClick = onBackClick,
onFollowClick = viewModel::followTopicToggle, onFollowClick = viewModel::followTopicToggle,
) onBookmarkChanged = viewModel::bookmarkNews,
)
} }
@VisibleForTesting @VisibleForTesting
@Composable @Composable
internal fun TopicScreen( internal fun TopicScreen(
topicState: TopicUiState, topicState: TopicUiState,
newsState: NewsUiState, newsUiState: NewsUiState,
onBackClick: () -> Unit, onBackClick: () -> Unit,
onFollowClick: (Boolean) -> Unit, onFollowClick: (Boolean) -> Unit,
onBookmarkChanged: (String, Boolean) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
LazyColumn( LazyColumn(
@ -111,8 +115,9 @@ internal fun TopicScreen(
TopicBody( TopicBody(
name = topicState.followableTopic.topic.name, name = topicState.followableTopic.topic.name,
description = topicState.followableTopic.topic.longDescription, description = topicState.followableTopic.topic.longDescription,
news = newsState, news = newsUiState,
imageUrl = topicState.followableTopic.topic.imageUrl imageUrl = topicState.followableTopic.topic.imageUrl,
onBookmarkChanged = onBookmarkChanged
) )
} }
} }
@ -126,14 +131,15 @@ private fun LazyListScope.TopicBody(
name: String, name: String,
description: String, description: String,
news: NewsUiState, news: NewsUiState,
imageUrl: String imageUrl: String,
onBookmarkChanged: (String, Boolean) -> Unit
) { ) {
// TODO: Show icon if available // TODO: Show icon if available
item { item {
TopicHeader(name, description, imageUrl) TopicHeader(name, description, imageUrl)
} }
TopicCards(news) TopicCards(news, onBookmarkChanged)
} }
@Composable @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) { when (news) {
is NewsUiState.Success -> { is NewsUiState.Success -> {
newsResourceCardItems( newsResourceCardItems(
items = news.news, items = news.news,
newsResourceMapper = { it }, newsResourceMapper = { it.newsResource },
isBookmarkedMapper = { /* TODO */ false }, isBookmarkedMapper = { it.isSaved },
onToggleBookmark = { /* TODO */ }, onToggleBookmark = { onBookmarkChanged(it.newsResource.id, !it.isSaved) },
itemModifier = Modifier.padding(24.dp) itemModifier = Modifier.padding(24.dp)
) )
} }
@ -187,7 +196,7 @@ private fun TopicBodyPreview() {
LazyColumn { LazyColumn {
TopicBody( TopicBody(
"Jetpack Compose", "Lorem ipsum maximum", "Jetpack Compose", "Lorem ipsum maximum",
NewsUiState.Success(emptyList()), "" NewsUiState.Success(emptyList()), "", { _, _ -> }
) )
} }
} }
@ -238,9 +247,16 @@ fun TopicScreenPopulated() {
NiaBackground { NiaBackground {
TopicScreen( TopicScreen(
topicState = TopicUiState.Success(FollowableTopic(previewTopics[0], false)), topicState = TopicUiState.Success(FollowableTopic(previewTopics[0], false)),
newsState = NewsUiState.Success(previewNewsResources), newsUiState = NewsUiState.Success(
onBackClick = {}, previewNewsResources.mapIndexed { index, newsResource ->
onFollowClick = {} SaveableNewsResource(
newsResource = newsResource,
isSaved = index % 2 == 0,
)
}
), onBackClick = {},
onFollowClick = {},
onBookmarkChanged = { _, _ -> },
) )
} }
} }
@ -256,9 +272,10 @@ fun TopicScreenLoading() {
NiaBackground { NiaBackground {
TopicScreen( TopicScreen(
topicState = TopicUiState.Loading, topicState = TopicUiState.Loading,
newsState = NewsUiState.Loading, newsUiState = NewsUiState.Loading,
onBackClick = {}, onBackClick = {},
onFollowClick = {} onFollowClick = {},
onBookmarkChanged = { _, _ -> },
) )
} }
} }

@ -19,11 +19,15 @@ package com.google.samples.apps.nowinandroid.feature.topic
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope 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.NewsRepository
import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository 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.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.FollowableTopic
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource 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.model.data.Topic
import com.google.samples.apps.nowinandroid.core.result.Result import com.google.samples.apps.nowinandroid.core.result.Result
import com.google.samples.apps.nowinandroid.core.result.asResult 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]) private val topicId: String = checkNotNull(savedStateHandle[TopicDestination.topicIdArg])
val topicUiState: StateFlow<TopicUiState> = topicUiStateStream(
topicId = topicId,
userDataRepository = userDataRepository,
topicsRepository = topicsRepository
)
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = TopicUiState.Loading
)
val newUiState: StateFlow<NewsUiState> = 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<TopicUiState> {
// Observe the followed topics, as they could change over time. // Observe the followed topics, as they could change over time.
private val followedTopicIdsStream: Flow<Result<Set<String>>> = val followedTopicIdsStream: Flow<Set<String>> =
userDataRepository.userDataStream userDataRepository.userDataStream
.map { it.followedTopics } .map { it.followedTopics }
.asResult()
// Observe topic information // Observe topic information
private val topic: Flow<Result<Topic>> = topicsRepository.getTopic(topicId).asResult() val topicStream: Flow<Topic> = topicsRepository.getTopic(
id = topicId
// Observe the News for this topic )
private val newsStream: Flow<Result<List<NewsResource>>> =
newsRepository.getNewsResourcesStream( return combine(
filterTopicIds = setOf(element = topicId), followedTopicIdsStream,
).asResult() topicStream,
::Pair
val uiState: StateFlow<TopicScreenUiState> = )
combine( .asResult()
followedTopicIdsStream, .map { followedTopicToTopicResult ->
topic, when (followedTopicToTopicResult) {
newsStream is Result.Success -> {
) { followedTopicsResult, topicResult, newsResult -> val (followedTopics, topic) = followedTopicToTopicResult.data
val topic: TopicUiState = val followed = followedTopics.contains(topicId)
if (topicResult is Result.Success && followedTopicsResult is Result.Success) {
val followed = followedTopicsResult.data.contains(topicId)
TopicUiState.Success( TopicUiState.Success(
followableTopic = FollowableTopic( followableTopic = FollowableTopic(
topic = topicResult.data, topic = topic,
isFollowed = followed isFollowed = followed
) )
) )
} else if ( }
topicResult is Result.Loading || followedTopicsResult is Result.Loading is Result.Loading -> {
) {
TopicUiState.Loading TopicUiState.Loading
} else { }
is Result.Error -> {
TopicUiState.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) { private fun newsUiStateStream(
viewModelScope.launch { topicId: String,
userDataRepository.toggleFollowedTopicId(topicId, followed) newsRepository: NewsRepository,
userDataRepository: UserDataRepository,
): Flow<NewsUiState> {
// Observe news
val newsStream: Flow<List<NewsResource>> = newsRepository.getNewsResourcesStream(
filterAuthorIds = emptySet(),
filterTopicIds = setOf(element = topicId),
)
// Observe bookmarks
val bookmarkStream: Flow<Set<String>> = 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 { sealed interface TopicUiState {
@ -114,12 +181,7 @@ sealed interface TopicUiState {
} }
sealed interface NewsUiState { sealed interface NewsUiState {
data class Success(val news: List<NewsResource>) : NewsUiState data class Success(val news: List<SaveableNewsResource>) : NewsUiState
object Error : NewsUiState object Error : NewsUiState
object Loading : NewsUiState object Loading : NewsUiState
} }
data class TopicScreenUiState(
val topicState: TopicUiState,
val newsState: NewsUiState
)

Loading…
Cancel
Save