Tim Malseed
03/21/2023, 4:20 AMTim Malseed
03/21/2023, 4:20 AMTim Malseed
03/21/2023, 4:20 AMsealed class ViewState {
object ShowScreenB : ViewState()
object ShowScreenC : ViewState()
}
@Composable
fun ScreenA(viewState: ViewState, onNavigateToScreenB: () -> Unit) {
when (viewState) {
is ShowScreenB -> {
LaunchedEffect(viewState) {
onNavigateToScreenB()
}
}
is ShowScreenC -> {
LaunchedEffect(viewState) {
onNavigateToScreenC()
}
}
}
}
@Composable
fun ScreenB(viewState: ViewState) {
when (viewState) {
is ShowScreenC -> {
LaunchedEffect(viewState) {
popBackStack()
}
}
}
}
Tim Malseed
03/21/2023, 4:21 AMShowScreenB
, we navigate to Screen B.
(2) If we’re on Screen A, and the ViewState changes to ShowScreenC
, we navigate to Screen C.
(3) If we’re on Screen B, and ViewState
changes to ShowScreenC
, we navigate back (and ideally, (2) will occur)Tim Malseed
03/21/2023, 4:22 AMShowScreenB
, and we begin navigating to Screen B, but then the viewState quickly updates to ShowScreenC
, Screen A might still attempt to navigate to Screen C (even though it’s currently navigating to Screen B). Then, Screen B tries to pop the backstack, and actually pops Screen C off the stack, and we end up back at Screen B.Tim Malseed
03/21/2023, 4:23 AMprivate fun NavBackStackEntry.lifecycleIsResumed() =
this.lifecycle.currentState == Lifecycle.State.RESUMED
private fun NavController.navigateSafely(from: NavBackStackEntry, route: String) {
if (from.lifecycleIsResumed()) {
navigate(route)
} else {
// Don't navigate
}
}
Tim Malseed
03/21/2023, 4:23 AMTim Malseed
03/21/2023, 4:24 AMShowScreenC
, but we’ve already launched the LaunchedEffect for ShowScreenC
, and ignored that navigation event. So now we’re stuck on Screen A!Ian Lake
03/21/2023, 5:22 AMTim Malseed
03/21/2023, 5:24 AMTim Malseed
03/21/2023, 5:26 AMTim Malseed
03/21/2023, 5:26 AMIan Lake
03/21/2023, 5:30 AMnavigateSafety
is the whole problem. Your entire handling of the event needs to be in that if statementIan Lake
03/21/2023, 5:30 AMIan Lake
03/21/2023, 5:34 AMnavigateSafety
in the first place - your entire handling, including your business logic of analytics, marking event as processed, etc. all need to be idempotent and in that same if block)Ian Lake
03/21/2023, 5:36 AMTim Malseed
03/21/2023, 5:37 AMshowScreenB
was just meant to help make this contrived example easier to understand. In my real life example, it is a ‘state’ of the application (like Unauthenticated), which causes the login screen to be displayedTim Malseed
03/21/2023, 5:38 AMnavigateSafely
doesn’t do anything in the ‘else’ block - if we can’t navigate, nothing happens and the event goes unhandled.Tim Malseed
03/21/2023, 5:39 AMTim Malseed
03/21/2023, 5:42 AMnavigateSafely
could return a boolean to indicate whether navigation was handled, that boolean could be held as some state, and the launched effect could be keyed against that state, and then conditionally fire only if ‘handled’ is falseTim Malseed
03/21/2023, 5:55 AMThis function should not be used to (re-)launch ongoing tasks in response to callback events by way of storing callback data in MutableState passed to key1.
Stylianos Gakis
03/21/2023, 8:44 AMcurioustechizen
03/21/2023, 8:51 AMTim Malseed
03/21/2023, 8:53 AMcurioustechizen
03/21/2023, 8:56 AMStylianos Gakis
03/21/2023, 8:59 AMcurioustechizen
03/21/2023, 8:59 AMif (isConnected) goToActualScreen() else goToConnectionScreen()
. I feel this logic belongs in the ViewModel (otherwise your higher level navigation component will need to have access to the entire app state).
How the goToActualScreen()
and goToConnectionScreen()
are implemented - that the ViewModel is not concerned with. That's the job of the navigation implementation, and it does have access to the navigation state as opposed to entire app state to do its job.curioustechizen
03/21/2023, 9:01 AMI am not confident that I would be able to provide it safely to the VM in a safe wayYes this can be tricky indeed - and this is at the core of the mistakes I alluded to above. I've seen some example apps on GitHub that achieve this better than how I do it but I haven't had the time to experiment with it.
Oleksandr Balan
03/21/2023, 1:06 PMtrySend
call. On the Compose UI part we simply collect effects
flow and call lambda, which then invokes appropriate navigation call.
private val _effects: Channel<Effect> = Channel(Channel.BUFFERED)
val effects: Flow<Effect> = _effects.receiveAsFlow()
fun saveIntroShown() {
viewModelScope.launch(<http://dispatchers.io|dispatchers.io>) {
preferences.featureIntroShown = true
_effects.trySend(Effect.CloseIntro)
}
}
sealed interface Effect {
object CloseIntro : Effect
}
Pablichjenkov
03/21/2023, 1:23 PMColton Idle
03/26/2023, 2:24 PMPablichjenkov
03/26/2023, 8:49 PMcurioustechizen
03/27/2023, 3:18 AMIan Lake
03/27/2023, 3:20 AMfindNavController
on is also unsafe to pass to a VM - they all contain references back to the activitycurioustechizen
03/27/2023, 4:17 AMfindNavController
just in time when you need to navigate.