Refactor - using `StateInViewModelScope()` extension function inside ViewModels

This commit also includes refactoring for functions, making functions more clear by extracting them inside ViewModels.
pull/454/head
Mohsen Rzna 3 years ago
parent c19b8b9319
commit 01351f1ef0

@ -22,12 +22,11 @@ import com.google.samples.apps.nowinandroid.MainActivityUiState.Loading
import com.google.samples.apps.nowinandroid.MainActivityUiState.Success
import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository
import com.google.samples.apps.nowinandroid.core.model.data.UserData
import com.google.samples.apps.nowinandroid.core.ui.stateInViewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
@HiltViewModel
class MainActivityViewModel @Inject constructor(
@ -35,11 +34,7 @@ class MainActivityViewModel @Inject constructor(
) : ViewModel() {
val uiState: StateFlow<MainActivityUiState> = userDataRepository.userDataStream.map {
Success(it)
}.stateIn(
scope = viewModelScope,
initialValue = Loading,
started = SharingStarted.WhileSubscribed(5_000)
)
}.stateInViewModelScope(viewModelScope, initialValue = Loading)
}
sealed interface MainActivityUiState {

@ -27,17 +27,19 @@ import com.google.samples.apps.nowinandroid.core.domain.model.FollowableAuthor
import com.google.samples.apps.nowinandroid.core.domain.model.SaveableNewsResource
import com.google.samples.apps.nowinandroid.core.model.data.Author
import com.google.samples.apps.nowinandroid.core.result.Result
import com.google.samples.apps.nowinandroid.core.result.Result.Error
import com.google.samples.apps.nowinandroid.core.result.Result.Loading
import com.google.samples.apps.nowinandroid.core.result.Result.Success
import com.google.samples.apps.nowinandroid.core.result.asResult
import com.google.samples.apps.nowinandroid.core.ui.stateInViewModelScope
import com.google.samples.apps.nowinandroid.feature.author.navigation.AuthorArgs
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class AuthorViewModel @Inject constructor(
@ -54,75 +56,67 @@ class AuthorViewModel @Inject constructor(
authorId = authorArgs.authorId,
userDataRepository = userDataRepository,
authorsRepository = authorsRepository
)
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = AuthorUiState.Loading
)
).stateInViewModelScope(viewModelScope, initialValue = AuthorUiState.Loading)
val newsUiState: StateFlow<NewsUiState> =
getSaveableNewsResourcesStream.newsUiStateStream(authorId = authorArgs.authorId)
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = NewsUiState.Loading
)
val newsUiState: StateFlow<NewsUiState> = getSaveableNewsResourcesStream
.newsUiStateStream(authorId = authorArgs.authorId)
.stateInViewModelScope(viewModelScope, initialValue = NewsUiState.Loading)
fun followAuthorToggle(followed: Boolean) {
viewModelScope.launch {
userDataRepository.toggleFollowedAuthorId(authorArgs.authorId, followed)
}
fun followAuthorToggle(followed: Boolean) = viewModelScope.launch {
userDataRepository.toggleFollowedAuthorId(authorArgs.authorId, followed)
}
fun bookmarkNews(newsResourceId: String, bookmarked: Boolean) {
viewModelScope.launch {
userDataRepository.updateNewsResourceBookmark(newsResourceId, bookmarked)
}
fun bookmarkNews(newsResourceId: String, bookmarked: Boolean) = viewModelScope.launch {
userDataRepository.updateNewsResourceBookmark(newsResourceId, bookmarked)
}
}
private fun authorUiStateStream(
authorId: String,
userDataRepository: UserDataRepository,
authorsRepository: AuthorsRepository,
): Flow<AuthorUiState> {
// Observe the followed authors, as they could change over time.
val followedAuthorIdsStream: Flow<Set<String>> =
userDataRepository.userDataStream
.map { it.followedAuthors }
// Observe author information
val authorStream: Flow<Author> = authorsRepository.getAuthorStream(
id = authorId
)
return combine(
followedAuthorIdsStream,
authorStream,
::Pair
)
.asResult()
.map { followedAuthorToAuthorResult ->
when (followedAuthorToAuthorResult) {
is Result.Success -> {
val (followedAuthors, author) = followedAuthorToAuthorResult.data
val followed = followedAuthors.contains(authorId)
AuthorUiState.Success(
followableAuthor = FollowableAuthor(
author = author,
isFollowed = followed
)
)
}
is Result.Loading -> {
AuthorUiState.Loading
}
is Result.Error -> {
AuthorUiState.Error
}
private fun authorUiStateStream(
authorId: String,
userDataRepository: UserDataRepository,
authorsRepository: AuthorsRepository,
): Flow<AuthorUiState> {
// Observe the followed authors, as they could change over time.
val followedAuthorIdsStream: Flow<Set<String>> =
userDataRepository.userDataStream.map { it.followedAuthors }
// Observe author information
val authorStream: Flow<Author> = authorsRepository.getAuthorStream(
id = authorId
)
return combine(
followedAuthorIdsStream,
authorStream,
::Pair
)
.asResult()
.map { followedAuthorToAuthorResult ->
handleToAuthorResult(followedAuthorToAuthorResult, authorId)
}
}
}
private fun handleToAuthorResult(
followedAuthorToAuthorResult: Result<Pair<Set<String>, Author>>,
authorId: String
) = when (followedAuthorToAuthorResult) {
is Success -> onSuccessResult(followedAuthorToAuthorResult, authorId)
is Loading -> AuthorUiState.Loading
is Error -> AuthorUiState.Error
}
private fun onSuccessResult(
followedAuthorToAuthorResult: Success<Pair<Set<String>, Author>>,
authorId: String
): AuthorUiState.Success {
val (followedAuthors, author) = followedAuthorToAuthorResult.data
val followed = followedAuthors.contains(authorId)
return AuthorUiState.Success(
followableAuthor = FollowableAuthor(
author = author,
isFollowed = followed
)
)
}
}
private fun GetSaveableNewsResourcesStreamUseCase.newsUiStateStream(
@ -134,9 +128,9 @@ private fun GetSaveableNewsResourcesStreamUseCase.newsUiStateStream(
).asResult()
.map { newsResult ->
when (newsResult) {
is Result.Success -> NewsUiState.Success(newsResult.data)
is Result.Loading -> NewsUiState.Loading
is Result.Error -> NewsUiState.Error
is Success -> NewsUiState.Success(newsResult.data)
is Loading -> NewsUiState.Loading
is Error -> NewsUiState.Error
}
}
}

@ -23,14 +23,13 @@ import com.google.samples.apps.nowinandroid.core.domain.GetSaveableNewsResources
import com.google.samples.apps.nowinandroid.core.domain.model.SaveableNewsResource
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState.Loading
import com.google.samples.apps.nowinandroid.core.ui.stateInViewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
@HiltViewModel
@ -44,15 +43,9 @@ class BookmarksViewModel @Inject constructor(
.map { newsResources -> newsResources.filter(SaveableNewsResource::isSaved) } // Only show bookmarked news resources.
.map<List<SaveableNewsResource>, NewsFeedUiState>(NewsFeedUiState::Success)
.onStart { emit(Loading) }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = Loading
)
.stateInViewModelScope(viewModelScope, initialValue = Loading)
fun removeFromSavedResources(newsResourceId: String) {
viewModelScope.launch {
userDataRepository.updateNewsResourceBookmark(newsResourceId, false)
}
fun removeFromSavedResources(newsResourceId: String) = viewModelScope.launch {
userDataRepository.updateNewsResourceBookmark(newsResourceId, false)
}
}

@ -25,17 +25,16 @@ import com.google.samples.apps.nowinandroid.core.domain.GetSaveableNewsResources
import com.google.samples.apps.nowinandroid.core.domain.GetSortedFollowableAuthorsStreamUseCase
import com.google.samples.apps.nowinandroid.core.domain.model.SaveableNewsResource
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState
import com.google.samples.apps.nowinandroid.core.ui.stateInViewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
@HiltViewModel
@ -50,12 +49,9 @@ class ForYouViewModel @Inject constructor(
private val shouldShowOnboarding: Flow<Boolean> =
userDataRepository.userDataStream.map { !it.shouldHideOnboarding }
val isSyncing = syncStatusMonitor.isSyncing
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = false
)
val isSyncing = syncStatusMonitor
.isSyncing
.stateInViewModelScope(viewModelScope, initialValue = false)
val feedState: StateFlow<NewsFeedUiState> =
userDataRepository.userDataStream
@ -79,11 +75,7 @@ class ForYouViewModel @Inject constructor(
// As the selected topics and topic state changes, this will cancel the old feed
// monitoring and start the new one.
.flatMapLatest { it }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = NewsFeedUiState.Loading
)
.stateInViewModelScope(viewModelScope, initialValue = NewsFeedUiState.Loading)
val onboardingUiState: StateFlow<OnboardingUiState> =
combine(
@ -99,23 +91,14 @@ class ForYouViewModel @Inject constructor(
} else {
OnboardingUiState.NotShown
}
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = OnboardingUiState.Loading
)
}.stateInViewModelScope(viewModelScope, initialValue = OnboardingUiState.Loading)
fun updateTopicSelection(topicId: String, isChecked: Boolean) {
viewModelScope.launch {
userDataRepository.toggleFollowedTopicId(topicId, isChecked)
}
fun updateTopicSelection(topicId: String, isChecked: Boolean) = viewModelScope.launch {
userDataRepository.toggleFollowedTopicId(topicId, isChecked)
}
fun updateAuthorSelection(authorId: String, isChecked: Boolean) {
viewModelScope.launch {
userDataRepository.toggleFollowedAuthorId(authorId, isChecked)
}
fun updateAuthorSelection(authorId: String, isChecked: Boolean) = viewModelScope.launch {
userDataRepository.toggleFollowedAuthorId(authorId, isChecked)
}
fun updateNewsResourceSaved(newsResourceId: String, isChecked: Boolean) {
@ -124,10 +107,8 @@ class ForYouViewModel @Inject constructor(
}
}
fun dismissOnboarding() {
viewModelScope.launch {
userDataRepository.setShouldHideOnboarding(true)
}
fun dismissOnboarding() = viewModelScope.launch {
userDataRepository.setShouldHideOnboarding(true)
}
}

@ -24,14 +24,13 @@ import com.google.samples.apps.nowinandroid.core.domain.GetSortedFollowableAutho
import com.google.samples.apps.nowinandroid.core.domain.TopicSortField
import com.google.samples.apps.nowinandroid.core.domain.model.FollowableAuthor
import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic
import com.google.samples.apps.nowinandroid.core.ui.stateInViewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
@ -54,22 +53,14 @@ class InterestsViewModel @Inject constructor(
getSortedFollowableAuthorsStream(),
getFollowableTopicsStream(sortBy = TopicSortField.NAME),
InterestsUiState::Interests
).stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = InterestsUiState.Loading
)
).stateInViewModelScope(viewModelScope, initialValue = InterestsUiState.Loading)
fun followTopic(followedTopicId: String, followed: Boolean) {
viewModelScope.launch {
userDataRepository.toggleFollowedTopicId(followedTopicId, followed)
}
fun followTopic(followedTopicId: String, followed: Boolean) = viewModelScope.launch {
userDataRepository.toggleFollowedTopicId(followedTopicId, followed)
}
fun followAuthor(followedAuthorId: String, followed: Boolean) {
viewModelScope.launch {
userDataRepository.toggleFollowedAuthorId(followedAuthorId, followed)
}
fun followAuthor(followedAuthorId: String, followed: Boolean) = viewModelScope.launch {
userDataRepository.toggleFollowedAuthorId(followedAuthorId, followed)
}
fun switchTab(newIndex: Int) {

@ -21,6 +21,7 @@ import androidx.lifecycle.viewModelScope
import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository
import com.google.samples.apps.nowinandroid.core.model.data.DarkThemeConfig
import com.google.samples.apps.nowinandroid.core.model.data.ThemeBrand
import com.google.samples.apps.nowinandroid.core.ui.stateInViewModelScope
import com.google.samples.apps.nowinandroid.feature.settings.SettingsUiState.Loading
import com.google.samples.apps.nowinandroid.feature.settings.SettingsUiState.Success
import dagger.hilt.android.lifecycle.HiltViewModel
@ -28,7 +29,6 @@ import javax.inject.Inject
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
@HiltViewModel
@ -44,29 +44,23 @@ class SettingsViewModel @Inject constructor(
darkThemeConfig = userData.darkThemeConfig
)
)
}
.stateIn(
scope = viewModelScope,
// Starting eagerly means the user data is ready when the SettingsDialog is laid out
// for the first time. Without this, due to b/221643630 the layout is done using the
// "Loading" text, then replaced with the user editable fields once loaded, however,
// the layout height doesn't change meaning all the fields are squashed into a small
// scrollable column.
// TODO: Change to SharingStarted.WhileSubscribed(5_000) when b/221643630 is fixed
started = SharingStarted.Eagerly,
initialValue = Loading
)
}.stateInViewModelScope(viewModelScope, SharingStarted.Eagerly, Loading)
fun updateThemeBrand(themeBrand: ThemeBrand) {
viewModelScope.launch {
userDataRepository.setThemeBrand(themeBrand)
}
/**
* Starting eagerly means the user data is ready when the SettingsDialog is laid out
* for the first time. Without this, due to b/221643630 the layout is done using the
* "Loading" text, then replaced with the user editable fields once loaded, however,
* the layout height doesn't change meaning all the fields are squashed into a small
* scrollable column.
* TODO: Change to SharingStarted.WhileSubscribed(5_000) when b/221643630 is fixed
*/
fun updateThemeBrand(themeBrand: ThemeBrand) = viewModelScope.launch {
userDataRepository.setThemeBrand(themeBrand)
}
fun updateDarkThemeConfig(darkThemeConfig: DarkThemeConfig) {
viewModelScope.launch {
userDataRepository.setDarkThemeConfig(darkThemeConfig)
}
fun updateDarkThemeConfig(darkThemeConfig: DarkThemeConfig) = viewModelScope.launch {
userDataRepository.setDarkThemeConfig(darkThemeConfig)
}
}

@ -27,16 +27,18 @@ import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic
import com.google.samples.apps.nowinandroid.core.domain.model.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.Result.Error
import com.google.samples.apps.nowinandroid.core.result.Result.Loading
import com.google.samples.apps.nowinandroid.core.result.Result.Success
import com.google.samples.apps.nowinandroid.core.result.asResult
import com.google.samples.apps.nowinandroid.core.ui.stateInViewModelScope
import com.google.samples.apps.nowinandroid.feature.topic.navigation.TopicArgs
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
@HiltViewModel
@ -45,7 +47,6 @@ class TopicViewModel @Inject constructor(
stringDecoder: StringDecoder,
private val userDataRepository: UserDataRepository,
topicsRepository: TopicsRepository,
// newsRepository: NewsRepository,
getSaveableNewsResourcesStream: GetSaveableNewsResourcesStreamUseCase
) : ViewModel() {
@ -55,34 +56,20 @@ class TopicViewModel @Inject constructor(
topicId = topicArgs.topicId,
userDataRepository = userDataRepository,
topicsRepository = topicsRepository
)
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = TopicUiState.Loading
)
).stateInViewModelScope(viewModelScope, initialValue = TopicUiState.Loading)
val newUiState: StateFlow<NewsUiState> = newsUiStateStream(
topicId = topicArgs.topicId,
userDataRepository = userDataRepository,
getSaveableNewsResourcesStream = getSaveableNewsResourcesStream
)
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = NewsUiState.Loading
)
).stateInViewModelScope(viewModelScope, initialValue = NewsUiState.Loading)
fun followTopicToggle(followed: Boolean) {
viewModelScope.launch {
userDataRepository.toggleFollowedTopicId(topicArgs.topicId, followed)
}
fun followTopicToggle(followed: Boolean) = viewModelScope.launch {
userDataRepository.toggleFollowedTopicId(topicArgs.topicId, followed)
}
fun bookmarkNews(newsResourceId: String, bookmarked: Boolean) {
viewModelScope.launch {
userDataRepository.updateNewsResourceBookmark(newsResourceId, bookmarked)
}
fun bookmarkNews(newsResourceId: String, bookmarked: Boolean) = viewModelScope.launch {
userDataRepository.updateNewsResourceBookmark(newsResourceId, bookmarked)
}
}
@ -92,9 +79,9 @@ private fun topicUiStateStream(
topicsRepository: TopicsRepository,
): Flow<TopicUiState> {
// Observe the followed topics, as they could change over time.
val followedTopicIdsStream: Flow<Set<String>> =
userDataRepository.userDataStream
.map { it.followedTopics }
val followedTopicIdsStream: Flow<Set<String>> = userDataRepository
.userDataStream
.map { it.followedTopics }
// Observe topic information
val topicStream: Flow<Topic> = topicsRepository.getTopic(
@ -108,27 +95,28 @@ private fun topicUiStateStream(
)
.asResult()
.map { followedTopicToTopicResult ->
when (followedTopicToTopicResult) {
is Result.Success -> {
val (followedTopics, topic) = followedTopicToTopicResult.data
val followed = followedTopics.contains(topicId)
TopicUiState.Success(
followableTopic = FollowableTopic(
topic = topic,
isFollowed = followed
)
)
}
is Result.Loading -> {
TopicUiState.Loading
}
is Result.Error -> {
TopicUiState.Error
}
}
handleTopicResult(followedTopicToTopicResult, topicId)
}
}
private fun handleTopicResult(
followedTopicToTopicResult: Result<Pair<Set<String>, Topic>>,
topicId: String
) = when (followedTopicToTopicResult) {
is Success -> {
val (followedTopics, topic) = followedTopicToTopicResult.data
val followed = followedTopics.contains(topicId)
TopicUiState.Success(
followableTopic = FollowableTopic(
topic = topic,
isFollowed = followed
)
)
}
is Loading -> TopicUiState.Loading
is Error -> TopicUiState.Error
}
private fun newsUiStateStream(
topicId: String,
getSaveableNewsResourcesStream: GetSaveableNewsResourcesStreamUseCase,
@ -156,12 +144,8 @@ private fun newsUiStateStream(
val (news, bookmarks) = newsToBookmarksResult.data
NewsUiState.Success(news)
}
is Result.Loading -> {
NewsUiState.Loading
}
is Result.Error -> {
NewsUiState.Error
}
is Result.Loading -> NewsUiState.Loading
is Result.Error -> NewsUiState.Error
}
}
}

Loading…
Cancel
Save