@@ -21,13 +21,18 @@ import androidx.lifecycle.viewModelScope
2121import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository
2222import com.google.samples.apps.nowinandroid.core.data.util.SyncStatusMonitor
2323import com.google.samples.apps.nowinandroid.core.domain.GetFollowableTopicsUseCase
24- import com.google.samples.apps.nowinandroid.core.domain.GetFollowedUserNewsResourcesUseCase
24+ import com.google.samples.apps.nowinandroid.core.domain.GetUserNewsResourcesUseCase
25+ import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource
26+ import com.google.samples.apps.nowinandroid.core.model.data.UserData
2527import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState
2628import dagger.hilt.android.lifecycle.HiltViewModel
2729import kotlinx.coroutines.flow.Flow
2830import kotlinx.coroutines.flow.SharingStarted
2931import kotlinx.coroutines.flow.StateFlow
3032import kotlinx.coroutines.flow.combine
33+ import kotlinx.coroutines.flow.distinctUntilChanged
34+ import kotlinx.coroutines.flow.flatMapLatest
35+ import kotlinx.coroutines.flow.flowOf
3136import kotlinx.coroutines.flow.map
3237import kotlinx.coroutines.flow.stateIn
3338import kotlinx.coroutines.launch
@@ -37,7 +42,7 @@ import javax.inject.Inject
3742class ForYouViewModel @Inject constructor(
3843 syncStatusMonitor : SyncStatusMonitor ,
3944 private val userDataRepository : UserDataRepository ,
40- getFollowedUserNewsResources : GetFollowedUserNewsResourcesUseCase ,
45+ private val getUserNewsResources : GetUserNewsResourcesUseCase ,
4146 getFollowableTopics : GetFollowableTopicsUseCase ,
4247) : ViewModel() {
4348
@@ -51,15 +56,13 @@ class ForYouViewModel @Inject constructor(
5156 initialValue = false ,
5257 )
5358
54- val feedState: StateFlow <NewsFeedUiState > =
55- getFollowedUserNewsResources().map {
56- NewsFeedUiState .Success (it)
57- }
58- .stateIn(
59- scope = viewModelScope,
60- started = SharingStarted .WhileSubscribed (5_000 ),
61- initialValue = NewsFeedUiState .Loading ,
62- )
59+ val feedState: StateFlow <NewsFeedUiState > = getFollowedUserNewsResources()
60+ .map(NewsFeedUiState ::Success )
61+ .stateIn(
62+ scope = viewModelScope,
63+ started = SharingStarted .WhileSubscribed (5_000 ),
64+ initialValue = NewsFeedUiState .Loading ,
65+ )
6366
6467 val onboardingUiState: StateFlow <OnboardingUiState > =
6568 combine(
@@ -95,4 +98,66 @@ class ForYouViewModel @Inject constructor(
9598 userDataRepository.setShouldHideOnboarding(true )
9699 }
97100 }
101+
102+ /* *
103+ * This sequence of flow transformation functions does the following:
104+ *
105+ * - map: maps the user data into a set of followed topic IDs or null if we should return
106+ * an empty list
107+ * - distinctUntilChanged: will only emit a set of followed topic IDs if it's changed. This
108+ * avoids calling potentially expensive operations (like setting up a new flow) when nothing
109+ * has changed.
110+ * - flatMapLatest: getUserNewsResources returns a flow, so we have a flow inside a
111+ * flow. flatMapLatest moves the inner flow (the one we want to return) to the outer flow
112+ * and cancels any previous flows created by getUserNewsResources.
113+ */
114+ private fun getFollowedUserNewsResources (): Flow <List <UserNewsResource >> =
115+ userDataRepository.userData
116+ .map { userData ->
117+ if (userData.shouldShowEmptyFeed()) {
118+ null
119+ } else {
120+ userData.followedTopics
121+ }
122+ }
123+ .distinctUntilChanged()
124+ .flatMapLatest { followedTopics ->
125+ if (followedTopics == null ) {
126+ flowOf(emptyList())
127+ } else {
128+ getUserNewsResources(filterTopicIds = followedTopics)
129+ }
130+ }
98131}
132+
133+ // Alternative approach (not currently being called)
134+ private fun Flow<UserData>.getFollowedUserNewsResources (
135+ getUserNewsResources : GetUserNewsResourcesUseCase ,
136+ ): Flow <List <UserNewsResource >> =
137+ map { userData ->
138+ if (userData.shouldShowEmptyFeed()) {
139+ null
140+ } else {
141+ userData.followedTopics
142+ }
143+ }
144+ .distinctUntilChanged()
145+ .flatMapLatest { followedTopics ->
146+ if (followedTopics == null ) {
147+ flowOf(emptyList())
148+ } else {
149+ getUserNewsResources(filterTopicIds = followedTopics)
150+ }
151+ }
152+
153+ /* *
154+ * If the user hasn't completed the onboarding and hasn't selected any interests
155+ * show an empty news list to clearly demonstrate that their selections affect the
156+ * news articles they will see.
157+ *
158+ * Note: It should not be possible for the user to get into a state where the onboarding
159+ * is not displayed AND they haven't followed any topics, however, this method is to safeguard
160+ * against that scenario in future.
161+ */
162+ private fun UserData.shouldShowEmptyFeed () =
163+ ! shouldHideOnboarding && followedTopics.isEmpty()
0 commit comments