Tim Malseed
03/21/2023, 12:15 PMTim Malseed
03/21/2023, 12:15 PMauthState
, which keeps track of whether the user is authorized/authenticated.
Then there’s the userState
, which is derived from the authState
, but does some additional work when authState
is authorized (attempts to download the user profile)
Then, in a ViewModel, I have a viewState
state flow. This is derived from userState
, and combines with some other flows, to produce ViewState.Loading
, ViewState.Login
, ViewState.Ready
, etc.
So, viewState
-> userState
-> authState
From virtually anywhere in the app, the user might decide to sign out. This calls a suspend function to sign out, which causes authState
to change to AuthState.Unauthorized
.
fun signOut() {
coroutineScope.launch {
viewModel.signOut()
navController.popBackStack(route = HomeDestination.route, inclusive = false)
}
}
The call to navController.popBackStack(HomeDestination.route)
, routes the user back to the home destination. This home destination observes the viewState
, and, because the authState
is AuthState.Unauthorized
, the viewState
should be ViewState.Login
, and the app should navigate to the login screen.
However, it seems that despite signOut()
having completed, the viewState
hasn’t yet updated. The home screen is still rendering its previous state. If that previous state should trigger navigation somewhere else, then that navigation occurs, we’re at the wrong destination, and the home destination is now on the back stack, stops observing the viewState
, and we don’t navigate to the login screen like we should.
If I put a small delay in between viewModel.signOut()
and navController.popBackStack()
, the issue goes away:
fun signOut() {
coroutineScope.launch {
viewModel.signOut()
delay(50)
navController.popBackStack(route = HomeDestination.route, inclusive = false)
}
}
Similarly, I could put a call to viewState.filter{ it is ViewState.Login}.first()
before popBackStack()
, and I’d at least ensure the viewState
is correct before navigating.
fun signOut() {
coroutineScope.launch {
viewModel.signOut()
viewState.filter{it is ViewState.Login}.first()
navController.popBackStack(route = HomeDestination.route, inclusive = false)
}
}
However, these don’t really feel right to me. The first is an arbitrary delay. How long should I wait for? And the second - well, what if we change the behaviour somewhere, and ViewState.Login
is no longer emitted after signing out? We’d suspend here forever.Tim Malseed
03/21/2023, 12:23 PMViewState
state flow to be out of sync with the AuthState
flow from which it is derived? Is it incorrect to assume that if one stateflow updates, then any derived stateflows should also update synchronously?
In other words, if signOut()
has completed, and authState
is Unauthorized
, is it possible that viewState
(derived from authState
) is yet to update? And that you could pop the backstack, go to a screen that observes viewState
, and viewState
is still yet to update?Tim Malseed
03/21/2023, 12:23 PMAlex Vanyo
03/21/2023, 4:37 PMDispatchers.Main.immediate
(either directly, or by default)?Alex Vanyo
03/21/2023, 4:50 PMDispatchers.Main
, one of them has to happen before the other. Dispatchers.Main.immediate
can make it even harder to follow, since some changes can start happening immediately, but it’s still sensitive to other operations you’re doingAlex Vanyo
03/21/2023, 4:53 PMmap { someSuspendingFunction() }
anywhere in your Flow
pipeline, your ordering can change depending on what someSuspendingFunction()
does and the dispatcher it runs onAlex Vanyo
03/21/2023, 5:03 PMIn other words, ifSo the answer is yes, with the details depending on exactly how “derived” is happeninghas completed, andsignOut()
isauthState
, is it possible thatUnauthorized
(derived fromviewState
) is yet to update?authState
Tim Malseed
03/21/2023, 8:07 PMmap { someSuspendingFunction() }
I understand - and I actually was searching for any of these to see if I could put a delay in the suspending function to reliably reproduce the problem (but there are none on this code path)
I’m not sure if I’m using Dispatchers.Main.Immediate
- I’m not using that directly at all. How would I know where it might be being used by default?
Generally speaking, when I create a StateFlow, I have a sort of ‘app level’ coroutine scope I use, which is made up of CoroutineScope(Dispatchers.Main + SupervisorJob() + CoroutineExceptionHandler {})
The ViewModel whose viewState
is derived from those state flows uses the viewModelScope
If they’re both being collected onAre you saying, if all derived flows are collected on, one of them has to happen before the otherDispatchers.Main
Dispatchers.Main
, then the ordering should be preserved? (Presumably because they can only run sequentially on that thread)Alex Vanyo
03/21/2023, 8:16 PMviewModelScope
uses Dispatchers.Main.immediate
by default
Here’s an example to play around with:
https://pl.kotl.in/NapRd7LEoAlex Vanyo
03/21/2023, 8:18 PMAre you saying, if all derived flows are collected onMore like “simultaneously” isn’t really possible with `Flow`s, there’s going to be some order they update it, and you probably don’t want to depend on that precise ordering (which you can influence with things like, then the ordering should be preserved?Dispatchers.Main
Dispatchers.Main.immediate
)
Even in a combine
of the same stateFlow
, you can get the old value and the new value, and not necessarily in a particular order.Tim Malseed
03/21/2023, 8:29 PMfun signOut() {
coroutineScope.launch {
viewModel.signOut()
navController.popBackStack(route = HomeDestination.route, inclusive = false)
onLaunchSignOutActivity()
}
}
Perhaps I should do something like:
LaunchedEffect(viewState) {
when(viewState is Login) {
navController.popBackStack(route = HomeDestination.route, inclusive = false)
}
}
(ViewState.Login
is the state rendered when the user is signed out)Tim Malseed
03/21/2023, 8:32 PMLogin
, then actually we should navigate to the login screen, rather than pop the backstack.
Perhaps part of the problem is trying to do some sort of global navigation handlingTim Malseed
03/21/2023, 8:36 PMLaunchedEffect(viewState) {
when (viewState is Login) {
if (navController.currentDestination != LoginDestination.route) {
navController.popBackStack(route = HomeDestination.route, inclusive = false)
}
}
}
Alex Vanyo
03/21/2023, 8:45 PMif (viewState is Login) {
// The login screen
} else {
// The logged in NavHost and screens
}
Tim Malseed
03/21/2023, 8:49 PMAlex Vanyo
03/21/2023, 8:51 PM