|
|
@ -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.repository.UserDataRepository
|
|
|
|
import com.google.samples.apps.nowinandroid.core.data.util.SyncStatusMonitor
|
|
|
|
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.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 com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState
|
|
|
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
|
|
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
|
|
|
import kotlinx.coroutines.flow.Flow
|
|
|
|
import kotlinx.coroutines.flow.Flow
|
|
|
|
import kotlinx.coroutines.flow.SharingStarted
|
|
|
|
import kotlinx.coroutines.flow.SharingStarted
|
|
|
|
import kotlinx.coroutines.flow.StateFlow
|
|
|
|
import kotlinx.coroutines.flow.StateFlow
|
|
|
|
import kotlinx.coroutines.flow.combine
|
|
|
|
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.map
|
|
|
|
import kotlinx.coroutines.flow.stateIn
|
|
|
|
import kotlinx.coroutines.flow.stateIn
|
|
|
|
import kotlinx.coroutines.launch
|
|
|
|
import kotlinx.coroutines.launch
|
|
|
@ -37,7 +42,7 @@ import javax.inject.Inject
|
|
|
|
class ForYouViewModel @Inject constructor(
|
|
|
|
class ForYouViewModel @Inject constructor(
|
|
|
|
syncStatusMonitor: SyncStatusMonitor,
|
|
|
|
syncStatusMonitor: SyncStatusMonitor,
|
|
|
|
private val userDataRepository: UserDataRepository,
|
|
|
|
private val userDataRepository: UserDataRepository,
|
|
|
|
getFollowedUserNewsResources: GetFollowedUserNewsResourcesUseCase,
|
|
|
|
private val getUserNewsResources: GetUserNewsResourcesUseCase,
|
|
|
|
getFollowableTopics: GetFollowableTopicsUseCase,
|
|
|
|
getFollowableTopics: GetFollowableTopicsUseCase,
|
|
|
|
) : ViewModel() {
|
|
|
|
) : ViewModel() {
|
|
|
|
|
|
|
|
|
|
|
@ -51,15 +56,13 @@ class ForYouViewModel @Inject constructor(
|
|
|
|
initialValue = false,
|
|
|
|
initialValue = false,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
val feedState: StateFlow<NewsFeedUiState> =
|
|
|
|
val feedState: StateFlow<NewsFeedUiState> = getFollowedUserNewsResources()
|
|
|
|
getFollowedUserNewsResources().map {
|
|
|
|
.map(NewsFeedUiState::Success)
|
|
|
|
NewsFeedUiState.Success(it)
|
|
|
|
.stateIn(
|
|
|
|
}
|
|
|
|
scope = viewModelScope,
|
|
|
|
.stateIn(
|
|
|
|
started = SharingStarted.WhileSubscribed(5_000),
|
|
|
|
scope = viewModelScope,
|
|
|
|
initialValue = NewsFeedUiState.Loading,
|
|
|
|
started = SharingStarted.WhileSubscribed(5_000),
|
|
|
|
)
|
|
|
|
initialValue = NewsFeedUiState.Loading,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
val onboardingUiState: StateFlow<OnboardingUiState> =
|
|
|
|
val onboardingUiState: StateFlow<OnboardingUiState> =
|
|
|
|
combine(
|
|
|
|
combine(
|
|
|
@ -95,4 +98,66 @@ class ForYouViewModel @Inject constructor(
|
|
|
|
userDataRepository.setShouldHideOnboarding(true)
|
|
|
|
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()
|
|
|
|