'Chaining Flows (collecting a Flow within the collect{} block of another Flow)
I'm new to using Flows. I have a situation where I need to wait on userLoginStatusChangedFlow to collect before calling readProfileFromFirestore() (which also collects a Flow). The first checks that the user is logged into Firebase Auth, while the second downloads the user's profile information from Firestore. The code I have works, but I'm not sure if I'm doing it in the intended way.
Question: Is it standard practice to "chain" Flows like this? Would you do it any differently?
init {
viewModelScope.launch {
repository.userLoginStatusChangedFlow.collect { userLoggedIn: Boolean? ->
if (userLoggedIn == true) {
launch {
readProfileFromFirestore()
}
} else {
navigateToLoginFragment()
}
}
}
}
The readProfileFromFirestore() method that gets called above:
// Download profile from Firestore and update the repository's cached profile.
private suspend fun readProfileFromFirestore() {
repository.readProfileFromFirestoreFlow().collect { state ->
when (state) {
is State.Success -> {
val profile: Models.Profile? = state.data
if (profile != null && repository.isProfileComplete(profile)) {
repository.updateCachedProfile(profile)
navigateToExploreFragment()
} else {
navigateToAuthenticationFragment()
}
}
is State.Failed -> {
// Error occurred while getting profile, so just inform user and go to LoginFragment.
displayErrorToast(state.throwable.message ?: "Failed to get profile")
navigateToAuthenticationFragment()
}
is State.Loading -> return@collect
}
}
}
Solution 1:[1]
There are a couple of unusual things about your code.
First, it's unusual that you have a Flow of a single value. A Flow is for multiple values that arrive over a period of time. A suspend function is much more sensible if there is only one item to retrieve. Both of your Flows seem to have this same odd behavior.
Second is that you're launching a coroutine from inside a coroutine when it's the last thing your coroutine needs to do. There's no reason to introduce extra complexity for this situation. Calling launch inside a coroutine might make sense if you need to start some side chain of actions when you still have more you want to do in the current coroutine without waiting for those other events. That's not the case here.
Occasionally, there is a need for a Flow with a finite number of items, but it's more common for Flows to be theoretically infinite, because they are polling some ongoing state. In these situations, it would be odd to try to do anything in the coroutine after collecting, because you don't know when or if that will happen.
So, looking at your code, I would say neither of your Flows should exist in the first place and you should replace them with suspend functions.
But if for some reason you couldn't do that (like maybe it's a Flow of state changes and you just want to listen in on what's the latest state of something, react to that state and ignore future state changes), then you can use first() instead of collect() to make your coroutine simpler, like:
init {
viewModelScope.launch {
val userLoggedIn = repository.userLoginStatusChangedFlow.first()
if (userLoggedIn == true) {
readProfileFromFirestore()
} else {
navigateToLoginFragment()
}
}
}
private suspend fun readProfileFromFirestore() {
val state = repository.readProfileFromFirestoreFlow().first()
when (state) {
//...
}
}
Notice this code is very similar to how it would look if you replaced your Flows with suspend functions. first() basically converts the Flow into a suspend function.
Solution 2:[2]
first, as @Tenfour04 pointed out, suspend function with first operator would be enough for your case,
if you insist on employing flow (which may be reasonable for cases like if you provide user-interactive retry), you may combine the two flows with operator flatMap-cluster (flatMapMerge / flatMapConcat / flatMapLatest) and use a single collect to consume value
val credentials = flow { emit(true) }
val profiles = flow { emit(State.Loading) }
credentials.flatMapMerge { value ->
if (!value) flow { emit(State.NotLogin) } else profiles
}.collect { state ->
// mapping your state here
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | Tenfour04 |
| Solution 2 | Minami |
