이전글 : https://witcheryoon.tistory.com/336
ViewModel을 컨트롤하는 Flow method들을 이해하면 만드는데 도움이 될 것이다.
설명페이지1 :Flow-kotlinx-coroutine-core : https://witcheryoon.tistory.com/292: 리스트 : SharedFlow / shareIn / onSubscription / conflate / stateFlow / toStates / scan / reduce / merge
0> Full code :
@OptIn(SavedStateHandleSaveableApi::class)
@HiltViewModel
class ForYouViewModel @Inject constructor(
syncStatusMonitor: SyncStatusMonitor,
private val userDataRepository: UserDataRepository,
private val getSaveableNewsResourcesStream: GetSaveableNewsResourcesStreamUseCase,
getSortedFollowableAuthorsStream: GetSortedFollowableAuthorsStreamUseCase,
getFollowableTopicsStream: GetFollowableTopicsStreamUseCase,
savedStateHandle: SavedStateHandle
) : ViewModel() {
private val followedInterestsUiState: StateFlow<FollowedInterestsUiState> =
userDataRepository.userDataStream
.map { userData ->
if (userData.followedAuthors.isEmpty() && userData.followedTopics.isEmpty()) {
None
} else {
FollowedInterests(
authorIds = userData.followedAuthors,
topicIds = userData.followedTopics
)
}
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = Unknown
)
/**
* The in-progress set of topics to be selected, persisted through process death with a
* [SavedStateHandle].
*/
private var inProgressTopicSelection by savedStateHandle.saveable {
mutableStateOf<Set<String>>(emptySet())
}
/**
* The in-progress set of authors to be selected, persisted through process death with a
* [SavedStateHandle].
*/
private var inProgressAuthorSelection by savedStateHandle.saveable {
mutableStateOf<Set<String>>(emptySet())
}
val isSyncing = syncStatusMonitor.isSyncing
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = false
)
val feedState: StateFlow<NewsFeedUiState> =
combine(
followedInterestsUiState,
snapshotFlow { inProgressTopicSelection },
snapshotFlow { inProgressAuthorSelection }
) { followedInterestsUiState, inProgressTopicSelection, inProgressAuthorSelection ->
when (followedInterestsUiState) {
// If we don't know the current selection state, emit loading.
Unknown -> flowOf<NewsFeedUiState>(NewsFeedUiState.Loading)
// If the user has followed topics, use those followed topics to populate the feed
is FollowedInterests -> {
getSaveableNewsResourcesStream(
filterTopicIds = followedInterestsUiState.topicIds,
filterAuthorIds = followedInterestsUiState.authorIds
).mapToFeedState()
}
// If the user hasn't followed interests yet, show a realtime populated feed based
// on the in-progress interests selections, if there are any.
None -> {
if (inProgressTopicSelection.isEmpty() && inProgressAuthorSelection.isEmpty()) {
flowOf<NewsFeedUiState>(NewsFeedUiState.Success(emptyList()))
} else {
getSaveableNewsResourcesStream(
filterTopicIds = inProgressTopicSelection,
filterAuthorIds = inProgressAuthorSelection
).mapToFeedState()
}
}
}
}
// Flatten the feed flows.
// As the selected topics and topic state changes, this will cancel the old feed
// monitoring and start the new one.
.flatMapLatest { it }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = NewsFeedUiState.Loading
)
val interestsSelectionUiState: StateFlow<ForYouInterestsSelectionUiState> =
combine(
followedInterestsUiState,
getFollowableTopicsStream(
followedTopicIdsStream = snapshotFlow { inProgressTopicSelection }
),
snapshotFlow { inProgressAuthorSelection }.flatMapLatest {
getSortedFollowableAuthorsStream(it)
}
) { followedInterestsUiState, topics, authors ->
when (followedInterestsUiState) {
Unknown -> ForYouInterestsSelectionUiState.Loading
is FollowedInterests -> ForYouInterestsSelectionUiState.NoInterestsSelection
None -> {
if (topics.isEmpty() && authors.isEmpty()) {
ForYouInterestsSelectionUiState.LoadFailed
} else {
ForYouInterestsSelectionUiState.WithInterestsSelection(
topics = topics,
authors = authors
)
}
}
}
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = ForYouInterestsSelectionUiState.Loading
)
fun updateTopicSelection(topicId: String, isChecked: Boolean) {
withMutableSnapshot {
inProgressTopicSelection =
// Update the in-progress selection based on whether the topic id was checked
if (isChecked) {
inProgressTopicSelection + topicId
} else {
inProgressTopicSelection - topicId
}
}
}
fun updateAuthorSelection(authorId: String, isChecked: Boolean) {
withMutableSnapshot {
inProgressAuthorSelection =
// Update the in-progress selection based on whether the author id was checked
if (isChecked) {
inProgressAuthorSelection + authorId
} else {
inProgressAuthorSelection - authorId
}
}
}
fun updateNewsResourceSaved(newsResourceId: String, isChecked: Boolean) {
viewModelScope.launch {
userDataRepository.updateNewsResourceBookmark(newsResourceId, isChecked)
}
}
fun saveFollowedInterests() {
// Don't attempt to save anything if nothing is selected
if (inProgressTopicSelection.isEmpty() && inProgressAuthorSelection.isEmpty()) {
return
}
viewModelScope.launch {
userDataRepository.setFollowedTopicIds(inProgressTopicSelection)
userDataRepository.setFollowedAuthorIds(inProgressAuthorSelection)
// Clear out the old selection, in case we return to onboarding
withMutableSnapshot {
inProgressTopicSelection = emptySet()
inProgressAuthorSelection = emptySet()
}
}
}
}
private fun Flow<List<SaveableNewsResource>>.mapToFeedState(): Flow<NewsFeedUiState> =
map<List<SaveableNewsResource>, NewsFeedUiState>(NewsFeedUiState::Success)
.onStart { emit(NewsFeedUiState.Loading) }
1> followedInterestUIState
Flow-kotlinx-coroutine-core의 StateFlow type의 UiState Hot Follow Emitter이다.
private val followedInterestsUiState: StateFlow<FollowedInterestsUiState> =
userDataRepository.userDataStream
.map { userData ->
if (userData.followedAuthors.isEmpty() && userData.followedTopics.isEmpty()) {
None
} else {
FollowedInterests(
authorIds = userData.followedAuthors,
topicIds = userData.followedTopics
)
}
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = Unknown
)
1) DI 로 generate 된 userDataRepository의 userDataStream method를 호출해 userData를 map으로 불러온 뒤 FollwedIntersts data class로 감싸서 FollowedInterestsUiState 타입으로 변형한다.
2) 이를 stateIn으로 viewmodelScope에서 동작하고 stopTimeout 5초, initialValue는 Unkown : FollowedInterestsUiState 상태를 가지는 hotFlow로 type change한다.
2> savedStateHandle, isSyncing
/**
* The in-progress set of topics to be selected, persisted through process death with a
* [SavedStateHandle].
*/
private var inProgressTopicSelection: Set<String> by savedStateHandle.saveable {
mutableStateOf<Set<String>>(emptySet())
}
/**
* The in-progress set of authors to be selected, persisted through process death with a
* [SavedStateHandle].
*/
private var inProgressAuthorSelection: Set<String> by savedStateHandle.saveable {
mutableStateOf<Set<String>>(emptySet())
}
val isSyncing = syncStatusMonitor.isSyncing
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = false
)
1) savedStateHandle로부터 Selection 상태에 대한 data 를 saveable을 통해서 불러와서 이전 state를 복구하는 변수를 만든다. Topic과 Author인 2부분을 가져왔다.
2) isSyncing는 isSycning을 1>의 followedInterestUIState와 같다. hotflow StateFlow<Boolean> 타입으로 선언한 모습이다.
3> feedState : NewsFeedUiState의 StateFlow
val feedState: StateFlow<NewsFeedUiState> =
combine(
followedInterestsUiState,
snapshotFlow { inProgressTopicSelection },
snapshotFlow { inProgressAuthorSelection }
) { followedInterestsUiState, inProgressTopicSelection, inProgressAuthorSelection ->
when (followedInterestsUiState) {
// If we don't know the current selection state, emit loading.
Unknown -> flowOf<NewsFeedUiState>(NewsFeedUiState.Loading)
// If the user has followed topics, use those followed topics to populate the feed
is FollowedInterests -> {
getSaveableNewsResourcesStream(
filterTopicIds = followedInterestsUiState.topicIds,
filterAuthorIds = followedInterestsUiState.authorIds
).mapToFeedState()
}
// If the user hasn't followed interests yet, show a realtime populated feed based
// on the in-progress interests selections, if there are any.
None -> {
if (inProgressTopicSelection.isEmpty() && inProgressAuthorSelection.isEmpty()) {
flowOf<NewsFeedUiState>(NewsFeedUiState.Success(emptyList()))
} else {
getSaveableNewsResourcesStream(
filterTopicIds = inProgressTopicSelection,
filterAuthorIds = inProgressAuthorSelection
).mapToFeedState()
}
}
}
}
// Flatten the feed flows.
// As the selected topics and topic state changes, this will cancel the old feed
// monitoring and start the new one.
.flatMapLatest { it }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = NewsFeedUiState.Loading
)
FollowedInterestsUiState는 아래의 3가지 State로 정의됨.
1> Unknown, 2> None, 그리고 data class instance인 3> FollwedInterests
FollowedInterestsUiState를 Transforming하는 mapToFeed의 구성은 아래와 같다
위는 GetSaveableNewsResourcesStreamUseCase.kt에서 invoke가 구현되어 있고 아래와 같다.
( invoke 함수를 통해 filterTopicIds: Set<String> filterTopicIds, filterAuthorIds: Set<String> 를 인자로 받는 customize된 apply method의 구현이 가능하여 mapToFeesState 사용 예와 같은 실행이 가능함 )
invoke호출시 repository에서 stream을 호출하여 List<SaveableNewsResource> 를 flow을 리턴한다는 것이고 mapToFeedState
class GetSaveableNewsResourcesStreamUseCase @Inject constructor(
private val newsRepository: NewsRepository,
userDataRepository: UserDataRepository
) {
private val bookmarkedNewsResourcesStream = userDataRepository.userDataStream.map {
it.bookmarkedNewsResources
}
/**
* Returns a list of SaveableNewsResources which match the supplied set of topic ids or author
* ids.
*
* @param filterTopicIds - A set of topic ids used to filter the list of news resources. If
* this is empty AND filterAuthorIds is empty the list of news resources will not be filtered.
* @param filterAuthorIds - A set of author ids used to filter the list of news resources. If
* this is empty AND filterTopicIds is empty the list of news resources will not be filtered.
*
*/
operator fun invoke(
filterTopicIds: Set<String> = emptySet(),
filterAuthorIds: Set<String> = emptySet()
): Flow<List<SaveableNewsResource>> =
if (filterTopicIds.isEmpty() && filterAuthorIds.isEmpty()) {
newsRepository.getNewsResourcesStream()
} else {
newsRepository.getNewsResourcesStream(
filterTopicIds = filterTopicIds,
filterAuthorIds = filterAuthorIds
)
}.mapToSaveableNewsResources(bookmarkedNewsResourcesStream)
}
private fun Flow<List<NewsResource>>.mapToSaveableNewsResources(
savedNewsResourceIdsStream: Flow<Set<String>>
): Flow<List<SaveableNewsResource>> =
filterNot { it.isEmpty() }
.combine(savedNewsResourceIdsStream) { newsResources, savedNewsResourceIds ->
newsResources.map { newsResource ->
SaveableNewsResource(
newsResource = newsResource,
isSaved = savedNewsResourceIds.contains(newsResource.id)
)
}
}
private fun Flow<List<SaveableNewsResource>>.mapToFeedState(): Flow<NewsFeedUiState> =
map<List<SaveableNewsResource>, NewsFeedUiState>(NewsFeedUiState::Success)
.onStart { emit(NewsFeedUiState.Loading) }
(onStart 참조 : https://witcheryoon.tistory.com/292)
UIState에 대한 sealed state는 아래와 같음.
FollowedInterestsUiState
/**
* A sealed hierarchy for the user's current followed interests state.
*/
sealed interface FollowedInterestsUiState {
/**
* The current state is unknown (hasn't loaded yet)
*/
object Unknown : FollowedInterestsUiState
/**
* The user hasn't followed any interests yet.
*/
object None : FollowedInterestsUiState
/**
* The user has followed the given (non-empty) set of [topicIds] or [authorIds].
*/
data class FollowedInterests(
val topicIds: Set<String>,
val authorIds: Set<String>
) : FollowedInterestsUiState
}
NewsFeedUiState
/**
* A sealed hierarchy describing the state of the feed of news resources.
*/
sealed interface NewsFeedUiState {
/**
* The feed is still loading.
*/
object Loading : NewsFeedUiState
/**
* The feed is loaded with the given list of news resources.
*/
data class Success(
/**
* The list of news resources contained in this feed.
*/
val feed: List<SaveableNewsResource>
) : NewsFeedUiState
}
위 UiState는 NewFeeds.kt에서 아래와 같이 UI에 따른 state변화로 view을 control할때 사용됨. 추후 이 부분을 중점적으로 리뷰 해야겠음
'Android Dev > NowInAndroid' 카테고리의 다른 글
NowInAndroid(4) - SyncWorker/SyncUtilities (0) | 2022.11.03 |
---|---|
NowInAndroid(3) - (Hilt) Viewmodel + DI overall flow, 재구성 예제 (0) | 2022.11.01 |
collectAsStateWithLifecycle 요약 (0) | 2022.10.28 |
SavedStateHandle 사용 (0) | 2022.10.25 |
NowInAndroid(1) - revision at 2022-11-14 (0) | 2022.10.25 |