Skip to content

Commit e536c39

Browse files
committed
Move new UseCase and tests into separate files
Change-Id: I4c337473ca0a60a5fccbb8aa640735cf7d616e71
1 parent e86cdea commit e536c39

File tree

4 files changed

+77
-120
lines changed

4 files changed

+77
-120
lines changed

core/domain/src/main/java/com/google/samples/apps/nowinandroid/core/domain/GetUserNewsResourcesUseCase.kt

Lines changed: 0 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,7 @@ import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
2424
import com.google.samples.apps.nowinandroid.core.model.data.UserData
2525
import kotlinx.coroutines.flow.Flow
2626
import kotlinx.coroutines.flow.combine
27-
import kotlinx.coroutines.flow.distinctUntilChanged
2827
import kotlinx.coroutines.flow.filterNot
29-
import kotlinx.coroutines.flow.flatMapLatest
30-
import kotlinx.coroutines.flow.flowOf
31-
import kotlinx.coroutines.flow.map
3228
import javax.inject.Inject
3329

3430
/**
@@ -55,56 +51,6 @@ class GetUserNewsResourcesUseCase @Inject constructor(
5551
}.mapToUserNewsResources(userDataRepository.userData)
5652
}
5753

58-
class GetFollowedUserNewsResourcesUseCase @Inject constructor(
59-
private val userDataRepository: UserDataRepository,
60-
val getUserNewsResources: GetUserNewsResourcesUseCase,
61-
) {
62-
/**
63-
* Returns a list of UserNewsResources for topics which the user is following
64-
*/
65-
operator fun invoke(): Flow<List<UserNewsResource>> =
66-
/**
67-
* This sequence of flow transformation functions does the following:
68-
*
69-
* - map: maps the user data into a set of followed topic IDs or null if we should return
70-
* an empty list
71-
* - distinctUntilChanged: will only emit a set of followed topic IDs if it's changed. This
72-
* avoids calling potentially expensive operations (like setting up a new flow) when nothing
73-
* has changed.
74-
* - flatMapLatest: getUserNewsResources returns a flow, so we have a flow inside a
75-
* flow. flatMapLatest moves the inner flow (the one we want to return) to the outer flow
76-
* and cancels any previous flows created by getUserNewsResources.
77-
*/
78-
userDataRepository.userData
79-
.map { userData ->
80-
if (shouldShowEmptyFeed(userData)) {
81-
null
82-
} else {
83-
userData.followedTopics
84-
}
85-
}
86-
.distinctUntilChanged()
87-
.flatMapLatest { followedTopics ->
88-
if (followedTopics == null) {
89-
flowOf(emptyList())
90-
} else {
91-
getUserNewsResources(filterTopicIds = followedTopics)
92-
}
93-
}
94-
95-
/**
96-
* If the user hasn't completed the onboarding and hasn't selected any interests
97-
* show an empty news list to clearly demonstrate that their selections affect the
98-
* news articles they will see.
99-
*
100-
* Note: It should not be possible for the user to get into a state where the onboarding
101-
* is not displayed AND they haven't followed any topics, however, this method is to safeguard
102-
* against that scenario in future.
103-
*/
104-
private fun shouldShowEmptyFeed(userData: UserData) =
105-
!userData.shouldHideOnboarding && userData.followedTopics.isEmpty()
106-
}
107-
10854
private fun Flow<List<NewsResource>>.mapToUserNewsResources(
10955
userDataStream: Flow<UserData>,
11056
): Flow<List<UserNewsResource>> =

core/domain/src/test/java/com/google/samples/apps/nowinandroid/core/domain/GetUserNewsResourcesUseCaseTest.kt

Lines changed: 0 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -83,55 +83,6 @@ class GetUserNewsResourcesUseCaseTest {
8383
}
8484
}
8585

86-
class GetFollowedUserNewsResourcesUseCaseTest {
87-
88-
@get:Rule
89-
val mainDispatcherRule = MainDispatcherRule()
90-
91-
private val newsRepository = TestNewsRepository()
92-
private val userDataRepository = TestUserDataRepository()
93-
private val getUserNewsResourcesUseCase =
94-
GetUserNewsResourcesUseCase(newsRepository, userDataRepository)
95-
96-
val useCase =
97-
GetFollowedUserNewsResourcesUseCase(userDataRepository, getUserNewsResourcesUseCase)
98-
99-
@Test
100-
fun whenOnboardingShownAndNoTopicsFollowed_emptyListIsReturned() = runTest {
101-
val followedNewsResources = useCase()
102-
103-
// Send some news resources and empty user data
104-
newsRepository.sendNewsResources(sampleNewsResources)
105-
userDataRepository.setUserData(emptyUserData)
106-
107-
// Check that an empty list is returned
108-
assertEquals(
109-
emptyList(),
110-
followedNewsResources.first(),
111-
)
112-
}
113-
114-
@Test
115-
fun whenTopicsAreFollowed_correctNewsResourcesAreReturned() = runTest {
116-
val followedNewsResources = useCase()
117-
118-
// Send some news resources and user data with a followed topic
119-
newsRepository.sendNewsResources(sampleNewsResources)
120-
121-
val userData = emptyUserData.copy(
122-
followedTopics = setOf(sampleTopic1.id),
123-
)
124-
userDataRepository.setUserData(userData)
125-
126-
assertEquals(
127-
sampleNewsResources
128-
.filter { it.topics.contains(sampleTopic1) }
129-
.mapToUserNewsResources(userData),
130-
followedNewsResources.first(),
131-
)
132-
}
133-
}
134-
13586
private val sampleTopic1 = Topic(
13687
id = "Topic1",
13788
name = "Headlines",

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

Lines changed: 76 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,18 @@ import androidx.lifecycle.viewModelScope
2121
import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository
2222
import com.google.samples.apps.nowinandroid.core.data.util.SyncStatusMonitor
2323
import 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
2527
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState
2628
import dagger.hilt.android.lifecycle.HiltViewModel
2729
import kotlinx.coroutines.flow.Flow
2830
import kotlinx.coroutines.flow.SharingStarted
2931
import kotlinx.coroutines.flow.StateFlow
3032
import kotlinx.coroutines.flow.combine
33+
import kotlinx.coroutines.flow.distinctUntilChanged
34+
import kotlinx.coroutines.flow.flatMapLatest
35+
import kotlinx.coroutines.flow.flowOf
3136
import kotlinx.coroutines.flow.map
3237
import kotlinx.coroutines.flow.stateIn
3338
import kotlinx.coroutines.launch
@@ -37,7 +42,7 @@ import javax.inject.Inject
3742
class 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()

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

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
package com.google.samples.apps.nowinandroid.feature.foryou
1818

1919
import com.google.samples.apps.nowinandroid.core.domain.GetFollowableTopicsUseCase
20-
import com.google.samples.apps.nowinandroid.core.domain.GetFollowedUserNewsResourcesUseCase
2120
import com.google.samples.apps.nowinandroid.core.domain.GetUserNewsResourcesUseCase
2221
import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic
2322
import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource
@@ -61,10 +60,6 @@ class ForYouViewModelTest {
6160
newsRepository = newsRepository,
6261
userDataRepository = userDataRepository,
6362
)
64-
private val getFollowedUserNewsResourcesUseCase = GetFollowedUserNewsResourcesUseCase(
65-
userDataRepository = userDataRepository,
66-
getUserNewsResources = getUserNewsResourcesUseCase,
67-
)
6863

6964
private val getFollowableTopicsUseCase = GetFollowableTopicsUseCase(
7065
topicsRepository = topicsRepository,
@@ -77,7 +72,7 @@ class ForYouViewModelTest {
7772
viewModel = ForYouViewModel(
7873
syncStatusMonitor = syncStatusMonitor,
7974
userDataRepository = userDataRepository,
80-
getFollowedUserNewsResources = getFollowedUserNewsResourcesUseCase,
75+
getUserNewsResources = getUserNewsResourcesUseCase,
8176
getFollowableTopics = getFollowableTopicsUseCase,
8277
)
8378
}

0 commit comments

Comments
 (0)