Skip to content

Commit e633942

Browse files
authored
Merge pull request #552 from android/dt/refactor-fyvm-2
Alternative approach to refactoring the ForYouViewModel
2 parents 478bc54 + f9dcdb4 commit e633942

File tree

2 files changed

+49
-22
lines changed

2 files changed

+49
-22
lines changed

feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModel.kt

Lines changed: 47 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,14 @@ import com.google.samples.apps.nowinandroid.core.data.util.SyncStatusMonitor
2323
import com.google.samples.apps.nowinandroid.core.domain.GetFollowableTopicsUseCase
2424
import com.google.samples.apps.nowinandroid.core.domain.GetUserNewsResourcesUseCase
2525
import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource
26+
import com.google.samples.apps.nowinandroid.core.model.data.UserData
2627
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState
2728
import dagger.hilt.android.lifecycle.HiltViewModel
2829
import kotlinx.coroutines.flow.Flow
2930
import kotlinx.coroutines.flow.SharingStarted
3031
import kotlinx.coroutines.flow.StateFlow
3132
import kotlinx.coroutines.flow.combine
33+
import kotlinx.coroutines.flow.distinctUntilChanged
3234
import kotlinx.coroutines.flow.flatMapLatest
3335
import kotlinx.coroutines.flow.flowOf
3436
import kotlinx.coroutines.flow.map
@@ -40,7 +42,7 @@ import javax.inject.Inject
4042
class ForYouViewModel @Inject constructor(
4143
syncStatusMonitor: SyncStatusMonitor,
4244
private val userDataRepository: UserDataRepository,
43-
private val getSaveableNewsResources: GetUserNewsResourcesUseCase,
45+
getUserNewsResources: GetUserNewsResourcesUseCase,
4446
getFollowableTopics: GetFollowableTopicsUseCase,
4547
) : ViewModel() {
4648

@@ -55,26 +57,8 @@ class ForYouViewModel @Inject constructor(
5557
)
5658

5759
val feedState: StateFlow<NewsFeedUiState> =
58-
userDataRepository.userData
59-
.map { userData ->
60-
// If the user hasn't completed the onboarding and hasn't selected any interests
61-
// show an empty news list to clearly demonstrate that their selections affect the
62-
// news articles they will see.
63-
if (!userData.shouldHideOnboarding &&
64-
userData.followedTopics.isEmpty()
65-
) {
66-
flowOf(NewsFeedUiState.Success(emptyList()))
67-
} else {
68-
getSaveableNewsResources(
69-
filterTopicIds = userData.followedTopics,
70-
)
71-
.map<List<UserNewsResource>, NewsFeedUiState>(NewsFeedUiState::Success)
72-
}
73-
}
74-
// Flatten the feed flows.
75-
// As the selected topics and topic state changes, this will cancel the old feed
76-
// monitoring and start the new one.
77-
.flatMapLatest { it }
60+
userDataRepository.getFollowedUserNewsResources(getUserNewsResources)
61+
.map(NewsFeedUiState::Success)
7862
.stateIn(
7963
scope = viewModelScope,
8064
started = SharingStarted.WhileSubscribed(5_000),
@@ -116,3 +100,45 @@ class ForYouViewModel @Inject constructor(
116100
}
117101
}
118102
}
103+
104+
/**
105+
* Obtain a flow of user news resources whose topics match those the user is following.
106+
*
107+
* getUserNewsResources: The `UseCase` used to obtain the flow of user news resources.
108+
*/
109+
private fun UserDataRepository.getFollowedUserNewsResources(
110+
getUserNewsResources: GetUserNewsResourcesUseCase,
111+
): Flow<List<UserNewsResource>> = userData
112+
// Map the user data into a set of followed topic IDs or null if we should return an empty list.
113+
.map { userData ->
114+
if (userData.shouldShowEmptyFeed()) {
115+
null
116+
} else {
117+
userData.followedTopics
118+
}
119+
}
120+
// Only emit a set of followed topic IDs if it's changed. This avoids calling potentially
121+
// expensive operations (like setting up a new flow) when nothing has changed.
122+
.distinctUntilChanged()
123+
// getUserNewsResources returns a flow, so we have a flow inside a flow. flatMapLatest moves
124+
// the inner flow (the one we want to return) to the outer flow and cancels any previous flows
125+
// created by getUserNewsResources.
126+
.flatMapLatest { followedTopics ->
127+
if (followedTopics == null) {
128+
flowOf(emptyList())
129+
} else {
130+
getUserNewsResources(filterTopicIds = followedTopics)
131+
}
132+
}
133+
134+
/**
135+
* If the user hasn't completed the onboarding and hasn't selected any interests
136+
* show an empty news list to clearly demonstrate that their selections affect the
137+
* news articles they will see.
138+
*
139+
* Note: It should not be possible for the user to get into a state where the onboarding
140+
* is not displayed AND they haven't followed any topics, however, this method is to safeguard
141+
* against that scenario in future.
142+
*/
143+
private fun UserData.shouldShowEmptyFeed() =
144+
!shouldHideOnboarding && followedTopics.isEmpty()

feature/foryou/src/test/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouViewModelTest.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ class ForYouViewModelTest {
6060
newsRepository = newsRepository,
6161
userDataRepository = userDataRepository,
6262
)
63+
6364
private val getFollowableTopicsUseCase = GetFollowableTopicsUseCase(
6465
topicsRepository = topicsRepository,
6566
userDataRepository = userDataRepository,
@@ -71,7 +72,7 @@ class ForYouViewModelTest {
7172
viewModel = ForYouViewModel(
7273
syncStatusMonitor = syncStatusMonitor,
7374
userDataRepository = userDataRepository,
74-
getSaveableNewsResources = getUserNewsResourcesUseCase,
75+
getUserNewsResources = getUserNewsResourcesUseCase,
7576
getFollowableTopics = getFollowableTopicsUseCase,
7677
)
7778
}

0 commit comments

Comments
 (0)