Move new UseCase and tests into separate files

Change-Id: I4c337473ca0a60a5fccbb8aa640735cf7d616e71
pull/552/head
Don Turner 2 years ago
parent e86cdea381
commit e536c396f9

@ -24,11 +24,7 @@ import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
import com.google.samples.apps.nowinandroid.core.model.data.UserData
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import javax.inject.Inject
/**
@ -55,56 +51,6 @@ class GetUserNewsResourcesUseCase @Inject constructor(
}.mapToUserNewsResources(userDataRepository.userData)
}
class GetFollowedUserNewsResourcesUseCase @Inject constructor(
private val userDataRepository: UserDataRepository,
val getUserNewsResources: GetUserNewsResourcesUseCase,
) {
/**
* Returns a list of UserNewsResources for topics which the user is following
*/
operator fun invoke(): Flow<List<UserNewsResource>> =
/**
* This sequence of flow transformation functions does the following:
*
* - map: maps the user data into a set of followed topic IDs or null if we should return
* an empty list
* - distinctUntilChanged: will only emit a set of followed topic IDs if it's changed. This
* avoids calling potentially expensive operations (like setting up a new flow) when nothing
* has changed.
* - flatMapLatest: getUserNewsResources returns a flow, so we have a flow inside a
* flow. flatMapLatest moves the inner flow (the one we want to return) to the outer flow
* and cancels any previous flows created by getUserNewsResources.
*/
userDataRepository.userData
.map { userData ->
if (shouldShowEmptyFeed(userData)) {
null
} else {
userData.followedTopics
}
}
.distinctUntilChanged()
.flatMapLatest { followedTopics ->
if (followedTopics == null) {
flowOf(emptyList())
} else {
getUserNewsResources(filterTopicIds = followedTopics)
}
}
/**
* If the user hasn't completed the onboarding and hasn't selected any interests
* show an empty news list to clearly demonstrate that their selections affect the
* news articles they will see.
*
* Note: It should not be possible for the user to get into a state where the onboarding
* is not displayed AND they haven't followed any topics, however, this method is to safeguard
* against that scenario in future.
*/
private fun shouldShowEmptyFeed(userData: UserData) =
!userData.shouldHideOnboarding && userData.followedTopics.isEmpty()
}
private fun Flow<List<NewsResource>>.mapToUserNewsResources(
userDataStream: Flow<UserData>,
): Flow<List<UserNewsResource>> =

@ -83,55 +83,6 @@ class GetUserNewsResourcesUseCaseTest {
}
}
class GetFollowedUserNewsResourcesUseCaseTest {
@get:Rule
val mainDispatcherRule = MainDispatcherRule()
private val newsRepository = TestNewsRepository()
private val userDataRepository = TestUserDataRepository()
private val getUserNewsResourcesUseCase =
GetUserNewsResourcesUseCase(newsRepository, userDataRepository)
val useCase =
GetFollowedUserNewsResourcesUseCase(userDataRepository, getUserNewsResourcesUseCase)
@Test
fun whenOnboardingShownAndNoTopicsFollowed_emptyListIsReturned() = runTest {
val followedNewsResources = useCase()
// Send some news resources and empty user data
newsRepository.sendNewsResources(sampleNewsResources)
userDataRepository.setUserData(emptyUserData)
// Check that an empty list is returned
assertEquals(
emptyList(),
followedNewsResources.first(),
)
}
@Test
fun whenTopicsAreFollowed_correctNewsResourcesAreReturned() = runTest {
val followedNewsResources = useCase()
// Send some news resources and user data with a followed topic
newsRepository.sendNewsResources(sampleNewsResources)
val userData = emptyUserData.copy(
followedTopics = setOf(sampleTopic1.id),
)
userDataRepository.setUserData(userData)
assertEquals(
sampleNewsResources
.filter { it.topics.contains(sampleTopic1) }
.mapToUserNewsResources(userData),
followedNewsResources.first(),
)
}
}
private val sampleTopic1 = Topic(
id = "Topic1",
name = "Headlines",

@ -21,13 +21,18 @@ import androidx.lifecycle.viewModelScope
import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository
import com.google.samples.apps.nowinandroid.core.data.util.SyncStatusMonitor
import com.google.samples.apps.nowinandroid.core.domain.GetFollowableTopicsUseCase
import com.google.samples.apps.nowinandroid.core.domain.GetFollowedUserNewsResourcesUseCase
import com.google.samples.apps.nowinandroid.core.domain.GetUserNewsResourcesUseCase
import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource
import com.google.samples.apps.nowinandroid.core.model.data.UserData
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
@ -37,7 +42,7 @@ import javax.inject.Inject
class ForYouViewModel @Inject constructor(
syncStatusMonitor: SyncStatusMonitor,
private val userDataRepository: UserDataRepository,
getFollowedUserNewsResources: GetFollowedUserNewsResourcesUseCase,
private val getUserNewsResources: GetUserNewsResourcesUseCase,
getFollowableTopics: GetFollowableTopicsUseCase,
) : ViewModel() {
@ -51,10 +56,8 @@ class ForYouViewModel @Inject constructor(
initialValue = false,
)
val feedState: StateFlow<NewsFeedUiState> =
getFollowedUserNewsResources().map {
NewsFeedUiState.Success(it)
}
val feedState: StateFlow<NewsFeedUiState> = getFollowedUserNewsResources()
.map(NewsFeedUiState::Success)
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
@ -95,4 +98,66 @@ class ForYouViewModel @Inject constructor(
userDataRepository.setShouldHideOnboarding(true)
}
}
/**
* This sequence of flow transformation functions does the following:
*
* - map: maps the user data into a set of followed topic IDs or null if we should return
* an empty list
* - distinctUntilChanged: will only emit a set of followed topic IDs if it's changed. This
* avoids calling potentially expensive operations (like setting up a new flow) when nothing
* has changed.
* - flatMapLatest: getUserNewsResources returns a flow, so we have a flow inside a
* flow. flatMapLatest moves the inner flow (the one we want to return) to the outer flow
* and cancels any previous flows created by getUserNewsResources.
*/
private fun getFollowedUserNewsResources(): Flow<List<UserNewsResource>> =
userDataRepository.userData
.map { userData ->
if (userData.shouldShowEmptyFeed()) {
null
} else {
userData.followedTopics
}
}
.distinctUntilChanged()
.flatMapLatest { followedTopics ->
if (followedTopics == null) {
flowOf(emptyList())
} else {
getUserNewsResources(filterTopicIds = followedTopics)
}
}
}
// Alternative approach (not currently being called)
private fun Flow<UserData>.getFollowedUserNewsResources(
getUserNewsResources: GetUserNewsResourcesUseCase,
): Flow<List<UserNewsResource>> =
map { userData ->
if (userData.shouldShowEmptyFeed()) {
null
} else {
userData.followedTopics
}
}
.distinctUntilChanged()
.flatMapLatest { followedTopics ->
if (followedTopics == null) {
flowOf(emptyList())
} else {
getUserNewsResources(filterTopicIds = followedTopics)
}
}
/**
* If the user hasn't completed the onboarding and hasn't selected any interests
* show an empty news list to clearly demonstrate that their selections affect the
* news articles they will see.
*
* Note: It should not be possible for the user to get into a state where the onboarding
* is not displayed AND they haven't followed any topics, however, this method is to safeguard
* against that scenario in future.
*/
private fun UserData.shouldShowEmptyFeed() =
!shouldHideOnboarding && followedTopics.isEmpty()

@ -17,7 +17,6 @@
package com.google.samples.apps.nowinandroid.feature.foryou
import com.google.samples.apps.nowinandroid.core.domain.GetFollowableTopicsUseCase
import com.google.samples.apps.nowinandroid.core.domain.GetFollowedUserNewsResourcesUseCase
import com.google.samples.apps.nowinandroid.core.domain.GetUserNewsResourcesUseCase
import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic
import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource
@ -61,10 +60,6 @@ class ForYouViewModelTest {
newsRepository = newsRepository,
userDataRepository = userDataRepository,
)
private val getFollowedUserNewsResourcesUseCase = GetFollowedUserNewsResourcesUseCase(
userDataRepository = userDataRepository,
getUserNewsResources = getUserNewsResourcesUseCase,
)
private val getFollowableTopicsUseCase = GetFollowableTopicsUseCase(
topicsRepository = topicsRepository,
@ -77,7 +72,7 @@ class ForYouViewModelTest {
viewModel = ForYouViewModel(
syncStatusMonitor = syncStatusMonitor,
userDataRepository = userDataRepository,
getFollowedUserNewsResources = getFollowedUserNewsResourcesUseCase,
getUserNewsResources = getUserNewsResourcesUseCase,
getFollowableTopics = getFollowableTopicsUseCase,
)
}

Loading…
Cancel
Save