Hey team, I’m still dealing with this compose nav...
# compose-android
t
Hey team, I’m still dealing with this compose nav problem, and it’s really doing my head in 😭 The app launches and shows the ‘start destination’, the home screen. It sees that the ‘viewState’ is ‘unauthorized’, so it navigates to the login screen (via launchedEffect, keyed on the viewState) We go to the login screen, and perform login, then pop the login screen off the back stack. As far as I can tell, this is all following navigation best practice. But, after popping back to the home screen, the viewState is still ‘unauthorized’ momentarily. Because we’ve navigated back to the home screen, the launched effect fires again, and we navigate once again to the login screen. It sees we’re authorized, navigates back to home. And now the home view state is up to date (authorized), and we render the home content. So, the navigation is home -> login -> home -> login -> home, when it should be home -> login -> home.
There are a couple of things that might be contributing. The viewState is derived in the ViewModel, from a StateFlow, which uses
stateIn(viewModelScope, whileSubscribed(5000), ViewState.Loading)
p
Make sure you update the authorization data/token before popping the login screen.
t
The login screen calls a suspend function to log in, and waits until that has completed before popping the back stack.
That log in function changes the ‘authorization state’ to ‘authorized’. This is exposed via a stateflow, the same stateflow that the home screen view model’s ‘view state’ is derived from
p
You mean login() right?
So what's the problem the state flow is not updating the value, does it have some operator applied that is buffering the events
Soon as you update the State flow from the login() function call it should override the previous state
t
So, to demonstrate the issue by way of logs:
Copy code
10:11:56.288  onDestinationChanged: home_route // start destination

10:11:56.450  AuthorizationState: Unauthorized // User is unauthorized

10:11:56.524  ViewState: Loading // Initial view state

10:11:56.803  ViewState: Login // Because user is unauthorized

10:11:56.803  navController.navigate(login_route) // Navigate to login screen

10:11:56.806  onDestinationChanged: login_route // User arrives. Presses 'login'

10:12:05.158  AuthorizationState: Authorized // Auth state changes to 'authorized'

10:12:05.726  navController.popBackStack() // go back to home screen

10:12:05.727  onDestinationChanged: home_route // we're back

10:12:05.745  ViewState: Login // ViewState is Login? Should be 'Ready'

10:12:05.745  navController.navigate(login_route) // Navigate back to the login screen

10:12:05.750  onDestinationChanged: login_route // Go to login screen

10:12:05.786  navController.popBackStack() // Oh, user is authorized, pop back again

10:12:05.788  onDestinationChanged: home_route // Back to home

10:12:05.837  ViewState: Ready // Now the home screen has the up-to-date view state
The stateflow from which ‘view state’ is derived is up-to-date. But when we go back to the home screen, the ‘viewState’ itself is stale
I’m starting to think I need to do something dumb, like `delay(50)`before calling
popBackStack()
p
What about adding another state, 'AwaitingLoginResult'. You would set the ViewState in that value soon as you navigate to the login screen. In that state you could show a loader or simply do nothing, just wait.
t
Yeah, that’s a better solution. Still, it feels like these are workarounds for a more fundamental problem
p
Some problems that are easily modeled with an event based system are harder to model in a representational state system. The only advantage I may see in the state modeling is testability. But right, you pretty much end up writing a state machine for any elemental things
I would rename Login State to RequestLogin State
t
I was hoping to have the view state derived from the user auth state. So, user auth is the ‘source of truth’. But, having to add in some intermediate state, like ‘AwaitingLoginResult’ means I now need to imperatively update the view state (as well as having it derived from the auth state). Maybe I’m thinking about this wrong, but it feels like a recipe for different bugs
Hmm, maybe not. I can’t actually think of a scenario where this would cause the view state to somehow be incorrect
p
I see it as a state machine with multiple inputs. Authorization data is one input but there could be other inputs like user events or so. The State of the View is derived from the response the machine reacts to any input change in the given state
In a perfect world only user auth would be sufficient but usually there are other variables involved.
Perhaps don't create an AwaitingLoginResult state, perhaps go to an Idle state, could be more convenient
Idle simply do nothing
t
Idle, or ‘loading’ is already the initial view state. I wonder if I can use something similar to
collectAsStateWithLifecycle()
, which resets its state when the lifecycle changes
In other words, if we come back to the home screen, and so its lifecycle changes to ‘started’, the view state is reset to ‘loading’
a
If you’re using
WhileSubscribed(5000)
, the previous value of the
StateFlow
will be cached and it won’t expire due to the default value for
replayExpiration
, the second parameter of
WhileSubscribed
So even if
Loading
is the default state, it’ll be caching the last one seen upon returning. Maybe for authentication state in particular, it’d be better to not cache that last value? Maybe set
replayExpiration
to
0
, and maybe also
stopTimeoutMillis
to
0
too specifically for the authentication:
Copy code
WhileSubscribed(0, 0)
That would ensure that you’re refreshing the flow upon going back to the home screen, which will cause the initial
Loading
state again
t
I tried setting
stopTimeoutMillis
to 0, but I didn’t even consider this
replayExpiration
parameter! That seems to solve the problem
I need to have a read of the
WhileSubscribed
docs and think about this a bit more
Alright, I guess this is a gotcha to watch out for. If you have a launched effect keyed by some state derived from a stateflow, consider how caching/replay of the sharingStarted property might affect that launched effect
c
I had to do this in my app recently. What I did was move the "authorized" state out of the viewModel and into "AppState" and then I only ever have a single source of truth on whether the user is auth'd or not. Works great!
t
I don’t mind that suggestion - although it’s not like the current approach has multiple sources of truth around whether the user is authed