Enable bookmarks on topics page

pull/301/head
Caren Chang 2 years ago
parent 5f82a32185
commit 31189f3328

@ -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 = { _, _ -> },
)
}
}

@ -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<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.
private val followedTopicIdsStream: Flow<Result<Set<String>>> =
val followedTopicIdsStream: Flow<Set<String>> =
userDataRepository.userDataStream
.map { it.followedTopics }
.asResult()
// Observe topic information
private val topic: Flow<Result<Topic>> = topicsRepository.getTopic(topicId).asResult()
// Observe the News for this topic
private val newsStream: Flow<Result<List<NewsResource>>> =
newsRepository.getNewsResourcesStream(
filterTopicIds = setOf(element = topicId),
).asResult()
val uiState: StateFlow<TopicScreenUiState> =
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<Topic> = 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<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 {
@ -114,12 +181,7 @@ sealed interface TopicUiState {
}
sealed interface NewsUiState {
data class Success(val news: List<NewsResource>) : NewsUiState
data class Success(val news: List<SaveableNewsResource>) : NewsUiState
object Error : NewsUiState
object Loading : NewsUiState
}
data class TopicScreenUiState(
val topicState: TopicUiState,
val newsState: NewsUiState
)

Loading…
Cancel
Save