From 01351f1ef07c26df44d419b72880437eca37f4cf Mon Sep 17 00:00:00 2001 From: Mohsen Rzna Date: Sun, 20 Nov 2022 16:34:46 +0100 Subject: [PATCH 1/9] Refactor - using `StateInViewModelScope()` extension function inside ViewModels This commit also includes refactoring for functions, making functions more clear by extracting them inside ViewModels. --- .../nowinandroid/MainActivityViewModel.kt | 9 +- .../feature/author/AuthorViewModel.kt | 130 +++++++++--------- .../feature/bookmarks/BookmarksViewModel.kt | 15 +- .../feature/foryou/ForYouViewModel.kt | 43 ++---- .../feature/interests/InterestsViewModel.kt | 21 +-- .../feature/settings/SettingsViewModel.kt | 36 ++--- .../feature/topic/TopicViewModel.kt | 84 +++++------ 7 files changed, 135 insertions(+), 203 deletions(-) diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/MainActivityViewModel.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/MainActivityViewModel.kt index 885fb6c3d5..ba343f228d 100644 --- a/app/src/main/java/com/google/samples/apps/nowinandroid/MainActivityViewModel.kt +++ b/app/src/main/java/com/google/samples/apps/nowinandroid/MainActivityViewModel.kt @@ -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 = userDataRepository.userDataStream.map { Success(it) - }.stateIn( - scope = viewModelScope, - initialValue = Loading, - started = SharingStarted.WhileSubscribed(5_000) - ) + }.stateInViewModelScope(viewModelScope, initialValue = Loading) } sealed interface MainActivityUiState { diff --git a/feature/author/src/main/java/com/google/samples/apps/nowinandroid/feature/author/AuthorViewModel.kt b/feature/author/src/main/java/com/google/samples/apps/nowinandroid/feature/author/AuthorViewModel.kt index 2f63dbc11e..c9b4652242 100644 --- a/feature/author/src/main/java/com/google/samples/apps/nowinandroid/feature/author/AuthorViewModel.kt +++ b/feature/author/src/main/java/com/google/samples/apps/nowinandroid/feature/author/AuthorViewModel.kt @@ -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 = - getSaveableNewsResourcesStream.newsUiStateStream(authorId = authorArgs.authorId) - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000), - initialValue = NewsUiState.Loading - ) + val newsUiState: StateFlow = 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 { - // Observe the followed authors, as they could change over time. - val followedAuthorIdsStream: Flow> = - userDataRepository.userDataStream - .map { it.followedAuthors } - - // Observe author information - val authorStream: Flow = 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 { + // Observe the followed authors, as they could change over time. + val followedAuthorIdsStream: Flow> = + userDataRepository.userDataStream.map { it.followedAuthors } + + // Observe author information + val authorStream: Flow = authorsRepository.getAuthorStream( + id = authorId + ) + + return combine( + followedAuthorIdsStream, + authorStream, + ::Pair + ) + .asResult() + .map { followedAuthorToAuthorResult -> + handleToAuthorResult(followedAuthorToAuthorResult, authorId) } - } + } + + private fun handleToAuthorResult( + followedAuthorToAuthorResult: Result, Author>>, + authorId: String + ) = when (followedAuthorToAuthorResult) { + is Success -> onSuccessResult(followedAuthorToAuthorResult, authorId) + is Loading -> AuthorUiState.Loading + is Error -> AuthorUiState.Error + } + + private fun onSuccessResult( + followedAuthorToAuthorResult: Success, 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 } } } diff --git a/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModel.kt b/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModel.kt index 1b9efc6aa9..bd41e05ee8 100644 --- a/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModel.kt +++ b/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModel.kt @@ -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, 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) } } diff --git a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModel.kt b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModel.kt index 599bfd8688..ac99ed5f9d 100644 --- a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModel.kt +++ b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModel.kt @@ -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 = 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 = 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 = 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) } } diff --git a/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsViewModel.kt b/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsViewModel.kt index 01d8b0d6e0..59aff907ba 100644 --- a/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsViewModel.kt +++ b/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsViewModel.kt @@ -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) { diff --git a/feature/settings/src/main/java/com/google/samples/apps/nowinandroid/feature/settings/SettingsViewModel.kt b/feature/settings/src/main/java/com/google/samples/apps/nowinandroid/feature/settings/SettingsViewModel.kt index fa159b5550..0338d66302 100644 --- a/feature/settings/src/main/java/com/google/samples/apps/nowinandroid/feature/settings/SettingsViewModel.kt +++ b/feature/settings/src/main/java/com/google/samples/apps/nowinandroid/feature/settings/SettingsViewModel.kt @@ -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) } } 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 160bcfb3ba..f1344234fa 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 @@ -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 = 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 { // Observe the followed topics, as they could change over time. - val followedTopicIdsStream: Flow> = - userDataRepository.userDataStream - .map { it.followedTopics } + val followedTopicIdsStream: Flow> = userDataRepository + .userDataStream + .map { it.followedTopics } // Observe topic information val topicStream: Flow = 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, 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 } } } From 35c7316433a9e533dbe99663ca89ef474939da5e Mon Sep 17 00:00:00 2001 From: Mohsen Rzna Date: Sun, 20 Nov 2022 16:35:50 +0100 Subject: [PATCH 2/9] Added two extension functions for StateIn There are two extension functions in this file, one is for ViewModels, and another one for using within a CoroutineScope. --- .../apps/nowinandroid/core/ui/Extenstions.kt | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/Extenstions.kt diff --git a/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/Extenstions.kt b/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/Extenstions.kt new file mode 100644 index 0000000000..ba0427b796 --- /dev/null +++ b/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/Extenstions.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.samples.apps.nowinandroid.core.ui + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.stateIn + +/** + * This is a helper extension function to make it easier to use [Flow.stateIn] within a ViewModel. + * @Returns a [StateFlow] that shares the latest value emitted by the original [Flow] and starts + * with the [initialValue]. + */ +fun Flow.stateInViewModelScope( + viewModelScope: CoroutineScope, + started: SharingStarted = SharingStarted.WhileSubscribed(5_000), + initialValue: T +): StateFlow = stateIn(viewModelScope, started, initialValue) + +/** + * This is a helper extension function to make it easier to use [Flow.stateIn] within a [CoroutineScope]. + * @Returns a [StateFlow] that shares the latest value emitted by the original [Flow] and starts + * with the [initialValue]. + */ +fun Flow.stateInCoroutineScope( + viewModelScope: CoroutineScope, + started: SharingStarted = SharingStarted.WhileSubscribed(5_000), + initialValue: T +): StateFlow = stateIn(viewModelScope, started, initialValue) From 4f1ac0a56159091f87776ef3736e1094ac25d86c Mon Sep 17 00:00:00 2001 From: Mohsen Rzna Date: Sun, 20 Nov 2022 16:37:01 +0100 Subject: [PATCH 3/9] Using extension function within `CoroutineScope` --- .../google/samples/apps/nowinandroid/ui/NiaAppState.kt | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaAppState.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaAppState.kt index bc5724c022..b505c9ccc5 100644 --- a/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaAppState.kt +++ b/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaAppState.kt @@ -36,6 +36,7 @@ import androidx.navigation.navOptions import androidx.tracing.trace import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor import com.google.samples.apps.nowinandroid.core.ui.TrackDisposableJank +import com.google.samples.apps.nowinandroid.core.ui.stateInCoroutineScope import com.google.samples.apps.nowinandroid.feature.bookmarks.navigation.bookmarksRoute import com.google.samples.apps.nowinandroid.feature.bookmarks.navigation.navigateToBookmarks import com.google.samples.apps.nowinandroid.feature.foryou.navigation.forYouNavigationRoute @@ -47,9 +48,7 @@ import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination.BOOKM import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination.FOR_YOU import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination.INTERESTS import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn @Composable fun rememberNiaAppState( @@ -95,11 +94,7 @@ class NiaAppState( val isOffline = networkMonitor.isOnline .map(Boolean::not) - .stateIn( - scope = coroutineScope, - started = SharingStarted.WhileSubscribed(5_000), - initialValue = false - ) + .stateInCoroutineScope(coroutineScope, initialValue = false) /** * Map of top level destinations to be used in the TopBar, BottomBar and NavRail. The key is the From cd2e3559666e46ffb95cea99262b7d01b361427e Mon Sep 17 00:00:00 2001 From: Mohsen Rzna Date: Sun, 20 Nov 2022 16:37:11 +0100 Subject: [PATCH 4/9] Spotless apply --- .../samples/apps/nowinandroid/feature/author/AuthorScreen.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/feature/author/src/main/java/com/google/samples/apps/nowinandroid/feature/author/AuthorScreen.kt b/feature/author/src/main/java/com/google/samples/apps/nowinandroid/feature/author/AuthorScreen.kt index 8f92a07d43..b53ca0e031 100644 --- a/feature/author/src/main/java/com/google/samples/apps/nowinandroid/feature/author/AuthorScreen.kt +++ b/feature/author/src/main/java/com/google/samples/apps/nowinandroid/feature/author/AuthorScreen.kt @@ -61,6 +61,8 @@ import com.google.samples.apps.nowinandroid.core.model.data.previewNewsResources import com.google.samples.apps.nowinandroid.core.ui.DevicePreviews import com.google.samples.apps.nowinandroid.core.ui.TrackScrollJank import com.google.samples.apps.nowinandroid.core.ui.newsResourceCardItems +import com.google.samples.apps.nowinandroid.feature.author.AuthorViewModel.AuthorUiState +import com.google.samples.apps.nowinandroid.feature.author.AuthorViewModel.NewsUiState @OptIn(ExperimentalLifecycleComposeApi::class) @Composable From c8e5d9b93deba0a705e21f8f92e67fc0b8acd24d Mon Sep 17 00:00:00 2001 From: Mohsen Rzna Date: Sun, 20 Nov 2022 16:49:12 +0100 Subject: [PATCH 5/9] Spotless apply --- .../samples/apps/nowinandroid/feature/author/AuthorViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/feature/author/src/main/java/com/google/samples/apps/nowinandroid/feature/author/AuthorViewModel.kt b/feature/author/src/main/java/com/google/samples/apps/nowinandroid/feature/author/AuthorViewModel.kt index c9b4652242..c6fabccab8 100644 --- a/feature/author/src/main/java/com/google/samples/apps/nowinandroid/feature/author/AuthorViewModel.kt +++ b/feature/author/src/main/java/com/google/samples/apps/nowinandroid/feature/author/AuthorViewModel.kt @@ -34,12 +34,12 @@ 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.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch -import javax.inject.Inject @HiltViewModel class AuthorViewModel @Inject constructor( From c5195ff3f46c1cb44ec86be9b85b84b19d0004a3 Mon Sep 17 00:00:00 2001 From: Mohsen Rzna Date: Sun, 20 Nov 2022 16:54:23 +0100 Subject: [PATCH 6/9] Lint fix --- .../samples/apps/nowinandroid/feature/author/AuthorScreen.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/feature/author/src/main/java/com/google/samples/apps/nowinandroid/feature/author/AuthorScreen.kt b/feature/author/src/main/java/com/google/samples/apps/nowinandroid/feature/author/AuthorScreen.kt index b53ca0e031..8f92a07d43 100644 --- a/feature/author/src/main/java/com/google/samples/apps/nowinandroid/feature/author/AuthorScreen.kt +++ b/feature/author/src/main/java/com/google/samples/apps/nowinandroid/feature/author/AuthorScreen.kt @@ -61,8 +61,6 @@ import com.google.samples.apps.nowinandroid.core.model.data.previewNewsResources import com.google.samples.apps.nowinandroid.core.ui.DevicePreviews import com.google.samples.apps.nowinandroid.core.ui.TrackScrollJank import com.google.samples.apps.nowinandroid.core.ui.newsResourceCardItems -import com.google.samples.apps.nowinandroid.feature.author.AuthorViewModel.AuthorUiState -import com.google.samples.apps.nowinandroid.feature.author.AuthorViewModel.NewsUiState @OptIn(ExperimentalLifecycleComposeApi::class) @Composable From 85bacab0578760361cc1d3de09c1eddbfcc1ac23 Mon Sep 17 00:00:00 2001 From: Mohsen Rzna Date: Sat, 26 Nov 2022 13:33:25 +0100 Subject: [PATCH 7/9] Refactor after code review Removed the other extension, and used it with different configuration everywhere. --- .../nowinandroid/MainActivityViewModel.kt | 4 ++-- .../apps/nowinandroid/ui/NiaAppState.kt | 4 ++-- .../apps/nowinandroid/core/ui/Extenstions.kt | 19 ++++--------------- .../feature/author/AuthorViewModel.kt | 6 +++--- .../feature/bookmarks/BookmarksViewModel.kt | 4 ++-- .../feature/foryou/ForYouViewModel.kt | 8 ++++---- .../feature/interests/InterestsViewModel.kt | 4 ++-- .../feature/settings/SettingsViewModel.kt | 4 ++-- .../feature/topic/TopicViewModel.kt | 12 ++++++------ 9 files changed, 27 insertions(+), 38 deletions(-) diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/MainActivityViewModel.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/MainActivityViewModel.kt index ba343f228d..c5368d0f13 100644 --- a/app/src/main/java/com/google/samples/apps/nowinandroid/MainActivityViewModel.kt +++ b/app/src/main/java/com/google/samples/apps/nowinandroid/MainActivityViewModel.kt @@ -22,7 +22,7 @@ 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 com.google.samples.apps.nowinandroid.core.ui.stateInScope import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlinx.coroutines.flow.StateFlow @@ -34,7 +34,7 @@ class MainActivityViewModel @Inject constructor( ) : ViewModel() { val uiState: StateFlow = userDataRepository.userDataStream.map { Success(it) - }.stateInViewModelScope(viewModelScope, initialValue = Loading) + }.stateInScope(viewModelScope, initialValue = Loading) } sealed interface MainActivityUiState { diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaAppState.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaAppState.kt index b505c9ccc5..a56bbc895a 100644 --- a/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaAppState.kt +++ b/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NiaAppState.kt @@ -36,7 +36,7 @@ import androidx.navigation.navOptions import androidx.tracing.trace import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor import com.google.samples.apps.nowinandroid.core.ui.TrackDisposableJank -import com.google.samples.apps.nowinandroid.core.ui.stateInCoroutineScope +import com.google.samples.apps.nowinandroid.core.ui.stateInScope import com.google.samples.apps.nowinandroid.feature.bookmarks.navigation.bookmarksRoute import com.google.samples.apps.nowinandroid.feature.bookmarks.navigation.navigateToBookmarks import com.google.samples.apps.nowinandroid.feature.foryou.navigation.forYouNavigationRoute @@ -94,7 +94,7 @@ class NiaAppState( val isOffline = networkMonitor.isOnline .map(Boolean::not) - .stateInCoroutineScope(coroutineScope, initialValue = false) + .stateInScope(coroutineScope, initialValue = false) /** * Map of top level destinations to be used in the TopBar, BottomBar and NavRail. The key is the diff --git a/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/Extenstions.kt b/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/Extenstions.kt index ba0427b796..dc848c1a54 100644 --- a/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/Extenstions.kt +++ b/core/ui/src/main/java/com/google/samples/apps/nowinandroid/core/ui/Extenstions.kt @@ -23,23 +23,12 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.stateIn /** - * This is a helper extension function to make it easier to use [Flow.stateIn] within a ViewModel. + * This is a helper extension function for making it easier to use [Flow.stateIn] within a ViewModel or etc. * @Returns a [StateFlow] that shares the latest value emitted by the original [Flow] and starts * with the [initialValue]. */ -fun Flow.stateInViewModelScope( - viewModelScope: CoroutineScope, +fun Flow.stateInScope( + coroutineScope: CoroutineScope, started: SharingStarted = SharingStarted.WhileSubscribed(5_000), initialValue: T -): StateFlow = stateIn(viewModelScope, started, initialValue) - -/** - * This is a helper extension function to make it easier to use [Flow.stateIn] within a [CoroutineScope]. - * @Returns a [StateFlow] that shares the latest value emitted by the original [Flow] and starts - * with the [initialValue]. - */ -fun Flow.stateInCoroutineScope( - viewModelScope: CoroutineScope, - started: SharingStarted = SharingStarted.WhileSubscribed(5_000), - initialValue: T -): StateFlow = stateIn(viewModelScope, started, initialValue) +): StateFlow = stateIn(coroutineScope, started, initialValue) diff --git a/feature/author/src/main/java/com/google/samples/apps/nowinandroid/feature/author/AuthorViewModel.kt b/feature/author/src/main/java/com/google/samples/apps/nowinandroid/feature/author/AuthorViewModel.kt index c6fabccab8..c64910f651 100644 --- a/feature/author/src/main/java/com/google/samples/apps/nowinandroid/feature/author/AuthorViewModel.kt +++ b/feature/author/src/main/java/com/google/samples/apps/nowinandroid/feature/author/AuthorViewModel.kt @@ -31,7 +31,7 @@ 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.core.ui.stateInScope import com.google.samples.apps.nowinandroid.feature.author.navigation.AuthorArgs import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject @@ -56,11 +56,11 @@ class AuthorViewModel @Inject constructor( authorId = authorArgs.authorId, userDataRepository = userDataRepository, authorsRepository = authorsRepository - ).stateInViewModelScope(viewModelScope, initialValue = AuthorUiState.Loading) + ).stateInScope(viewModelScope, initialValue = AuthorUiState.Loading) val newsUiState: StateFlow = getSaveableNewsResourcesStream .newsUiStateStream(authorId = authorArgs.authorId) - .stateInViewModelScope(viewModelScope, initialValue = NewsUiState.Loading) + .stateInScope(viewModelScope, initialValue = NewsUiState.Loading) fun followAuthorToggle(followed: Boolean) = viewModelScope.launch { userDataRepository.toggleFollowedAuthorId(authorArgs.authorId, followed) diff --git a/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModel.kt b/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModel.kt index bd41e05ee8..7e9e39ac9e 100644 --- a/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModel.kt +++ b/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModel.kt @@ -23,7 +23,7 @@ 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 com.google.samples.apps.nowinandroid.core.ui.stateInScope import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlinx.coroutines.flow.StateFlow @@ -43,7 +43,7 @@ class BookmarksViewModel @Inject constructor( .map { newsResources -> newsResources.filter(SaveableNewsResource::isSaved) } // Only show bookmarked news resources. .map, NewsFeedUiState>(NewsFeedUiState::Success) .onStart { emit(Loading) } - .stateInViewModelScope(viewModelScope, initialValue = Loading) + .stateInScope(viewModelScope, initialValue = Loading) fun removeFromSavedResources(newsResourceId: String) = viewModelScope.launch { userDataRepository.updateNewsResourceBookmark(newsResourceId, false) diff --git a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModel.kt b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModel.kt index ac99ed5f9d..2c1d817379 100644 --- a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModel.kt +++ b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModel.kt @@ -25,7 +25,7 @@ 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 com.google.samples.apps.nowinandroid.core.ui.stateInScope import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlinx.coroutines.flow.Flow @@ -51,7 +51,7 @@ class ForYouViewModel @Inject constructor( val isSyncing = syncStatusMonitor .isSyncing - .stateInViewModelScope(viewModelScope, initialValue = false) + .stateInScope(viewModelScope, initialValue = false) val feedState: StateFlow = userDataRepository.userDataStream @@ -75,7 +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 } - .stateInViewModelScope(viewModelScope, initialValue = NewsFeedUiState.Loading) + .stateInScope(viewModelScope, initialValue = NewsFeedUiState.Loading) val onboardingUiState: StateFlow = combine( @@ -91,7 +91,7 @@ class ForYouViewModel @Inject constructor( } else { OnboardingUiState.NotShown } - }.stateInViewModelScope(viewModelScope, initialValue = OnboardingUiState.Loading) + }.stateInScope(viewModelScope, initialValue = OnboardingUiState.Loading) fun updateTopicSelection(topicId: String, isChecked: Boolean) = viewModelScope.launch { userDataRepository.toggleFollowedTopicId(topicId, isChecked) diff --git a/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsViewModel.kt b/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsViewModel.kt index 59aff907ba..523918fd43 100644 --- a/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsViewModel.kt +++ b/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsViewModel.kt @@ -24,7 +24,7 @@ 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 com.google.samples.apps.nowinandroid.core.ui.stateInScope import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow @@ -53,7 +53,7 @@ class InterestsViewModel @Inject constructor( getSortedFollowableAuthorsStream(), getFollowableTopicsStream(sortBy = TopicSortField.NAME), InterestsUiState::Interests - ).stateInViewModelScope(viewModelScope, initialValue = InterestsUiState.Loading) + ).stateInScope(viewModelScope, initialValue = InterestsUiState.Loading) fun followTopic(followedTopicId: String, followed: Boolean) = viewModelScope.launch { userDataRepository.toggleFollowedTopicId(followedTopicId, followed) diff --git a/feature/settings/src/main/java/com/google/samples/apps/nowinandroid/feature/settings/SettingsViewModel.kt b/feature/settings/src/main/java/com/google/samples/apps/nowinandroid/feature/settings/SettingsViewModel.kt index 0338d66302..4cec31dde7 100644 --- a/feature/settings/src/main/java/com/google/samples/apps/nowinandroid/feature/settings/SettingsViewModel.kt +++ b/feature/settings/src/main/java/com/google/samples/apps/nowinandroid/feature/settings/SettingsViewModel.kt @@ -21,7 +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.core.ui.stateInScope 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 @@ -44,7 +44,7 @@ class SettingsViewModel @Inject constructor( darkThemeConfig = userData.darkThemeConfig ) ) - }.stateInViewModelScope(viewModelScope, SharingStarted.Eagerly, Loading) + }.stateInScope(viewModelScope, SharingStarted.Eagerly, Loading) /** * Starting eagerly means the user data is ready when the SettingsDialog is laid out 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 f1344234fa..546548f032 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 @@ -31,7 +31,7 @@ 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.core.ui.stateInScope import com.google.samples.apps.nowinandroid.feature.topic.navigation.TopicArgs import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject @@ -56,13 +56,13 @@ class TopicViewModel @Inject constructor( topicId = topicArgs.topicId, userDataRepository = userDataRepository, topicsRepository = topicsRepository - ).stateInViewModelScope(viewModelScope, initialValue = TopicUiState.Loading) + ).stateInScope(viewModelScope, initialValue = TopicUiState.Loading) val newUiState: StateFlow = newsUiStateStream( topicId = topicArgs.topicId, userDataRepository = userDataRepository, getSaveableNewsResourcesStream = getSaveableNewsResourcesStream - ).stateInViewModelScope(viewModelScope, initialValue = NewsUiState.Loading) + ).stateInScope(viewModelScope, initialValue = NewsUiState.Loading) fun followTopicToggle(followed: Boolean) = viewModelScope.launch { userDataRepository.toggleFollowedTopicId(topicArgs.topicId, followed) @@ -140,12 +140,12 @@ private fun newsUiStateStream( .asResult() .map { newsToBookmarksResult -> when (newsToBookmarksResult) { - is Result.Success -> { + is Success -> { val (news, bookmarks) = newsToBookmarksResult.data NewsUiState.Success(news) } - is Result.Loading -> NewsUiState.Loading - is Result.Error -> NewsUiState.Error + is Loading -> NewsUiState.Loading + is Error -> NewsUiState.Error } } } From d946b5c1ef12a77ce0a57015683b8faf72141f27 Mon Sep 17 00:00:00 2001 From: Mohsen Rzna Date: Sat, 26 Nov 2022 13:36:41 +0100 Subject: [PATCH 8/9] Renaming This shouldn't be starting with a capital keyword, it's an extension. --- .../samples/apps/nowinandroid/feature/topic/TopicScreen.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 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 61bd13aa09..68acb05110 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 @@ -119,7 +119,7 @@ internal fun TopicScreen( uiState = topicUiState.followableTopic, ) } - TopicBody( + topicBody( name = topicUiState.followableTopic.topic.name, description = topicUiState.followableTopic.topic.longDescription, news = newsUiState, @@ -134,7 +134,7 @@ internal fun TopicScreen( } } -private fun LazyListScope.TopicBody( +private fun LazyListScope.topicBody( name: String, description: String, news: NewsUiState, @@ -204,7 +204,7 @@ private fun LazyListScope.TopicCards( private fun TopicBodyPreview() { NiaTheme { LazyColumn { - TopicBody( + topicBody( "Jetpack Compose", "Lorem ipsum maximum", NewsUiState.Success(emptyList()), "", { _, _ -> } ) From 9e8b60187d68a23e4185619b1e26de581db35759 Mon Sep 17 00:00:00 2001 From: Mohsen Rzna Date: Sat, 26 Nov 2022 13:50:31 +0100 Subject: [PATCH 9/9] Refactoring for expression body and reverting back the expression body This was causing a leak of a job, and it was supposed to return Unit! --- .../feature/author/AuthorViewModel.kt | 12 +++-- .../feature/bookmarks/BookmarksViewModel.kt | 6 ++- .../feature/foryou/ForYouViewModel.kt | 18 ++++--- .../feature/interests/InterestsViewModel.kt | 12 +++-- .../feature/settings/SettingsViewModel.kt | 49 +++++++++++-------- .../feature/topic/TopicViewModel.kt | 12 +++-- 6 files changed, 69 insertions(+), 40 deletions(-) diff --git a/feature/author/src/main/java/com/google/samples/apps/nowinandroid/feature/author/AuthorViewModel.kt b/feature/author/src/main/java/com/google/samples/apps/nowinandroid/feature/author/AuthorViewModel.kt index c64910f651..f73cf818ce 100644 --- a/feature/author/src/main/java/com/google/samples/apps/nowinandroid/feature/author/AuthorViewModel.kt +++ b/feature/author/src/main/java/com/google/samples/apps/nowinandroid/feature/author/AuthorViewModel.kt @@ -62,12 +62,16 @@ class AuthorViewModel @Inject constructor( .newsUiStateStream(authorId = authorArgs.authorId) .stateInScope(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( diff --git a/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModel.kt b/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModel.kt index 7e9e39ac9e..da86508482 100644 --- a/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModel.kt +++ b/feature/bookmarks/src/main/java/com/google/samples/apps/nowinandroid/feature/bookmarks/BookmarksViewModel.kt @@ -45,7 +45,9 @@ class BookmarksViewModel @Inject constructor( .onStart { emit(Loading) } .stateInScope(viewModelScope, initialValue = Loading) - fun removeFromSavedResources(newsResourceId: String) = viewModelScope.launch { - userDataRepository.updateNewsResourceBookmark(newsResourceId, false) + fun removeFromSavedResources(newsResourceId: String) { + viewModelScope.launch { + userDataRepository.updateNewsResourceBookmark(newsResourceId, false) + } } } diff --git a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModel.kt b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModel.kt index 2c1d817379..1f92b1e986 100644 --- a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModel.kt +++ b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModel.kt @@ -93,12 +93,16 @@ class ForYouViewModel @Inject constructor( } }.stateInScope(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) { @@ -107,8 +111,10 @@ class ForYouViewModel @Inject constructor( } } - fun dismissOnboarding() = viewModelScope.launch { - userDataRepository.setShouldHideOnboarding(true) + fun dismissOnboarding() { + viewModelScope.launch { + userDataRepository.setShouldHideOnboarding(true) + } } } diff --git a/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsViewModel.kt b/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsViewModel.kt index 523918fd43..b4a7050fe7 100644 --- a/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsViewModel.kt +++ b/feature/interests/src/main/java/com/google/samples/apps/nowinandroid/feature/interests/InterestsViewModel.kt @@ -55,12 +55,16 @@ class InterestsViewModel @Inject constructor( InterestsUiState::Interests ).stateInScope(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) { diff --git a/feature/settings/src/main/java/com/google/samples/apps/nowinandroid/feature/settings/SettingsViewModel.kt b/feature/settings/src/main/java/com/google/samples/apps/nowinandroid/feature/settings/SettingsViewModel.kt index 4cec31dde7..4d24665d95 100644 --- a/feature/settings/src/main/java/com/google/samples/apps/nowinandroid/feature/settings/SettingsViewModel.kt +++ b/feature/settings/src/main/java/com/google/samples/apps/nowinandroid/feature/settings/SettingsViewModel.kt @@ -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.model.data.UserData import com.google.samples.apps.nowinandroid.core.ui.stateInScope import com.google.samples.apps.nowinandroid.feature.settings.SettingsUiState.Loading import com.google.samples.apps.nowinandroid.feature.settings.SettingsUiState.Success @@ -37,30 +38,38 @@ class SettingsViewModel @Inject constructor( ) : ViewModel() { val settingsUiState: StateFlow = userDataRepository.userDataStream - .map { userData -> - Success( - settings = UserEditableSettings( - brand = userData.themeBrand, - darkThemeConfig = userData.darkThemeConfig - ) - ) - }.stateInScope(viewModelScope, SharingStarted.Eagerly, Loading) + .map { userData -> mapToSuccess(userData) } + .stateInScope( + 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 + */ + SharingStarted.Eagerly, Loading + ) - /** - * 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 - */ + private fun mapToSuccess(userData: UserData) = + Success( + settings = UserEditableSettings( + brand = userData.themeBrand, + darkThemeConfig = userData.darkThemeConfig + ) + ) - fun updateThemeBrand(themeBrand: ThemeBrand) = viewModelScope.launch { - userDataRepository.setThemeBrand(themeBrand) + 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) + } } } 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 546548f032..e2b0b4f197 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 @@ -64,12 +64,16 @@ class TopicViewModel @Inject constructor( getSaveableNewsResourcesStream = getSaveableNewsResourcesStream ).stateInScope(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) + } } }