Navigation 3 - Part 1 - Buggy Old Example 2
Broken Example 2
I want to publish this in addition to the original blog post so you can see my first broken solution. I think many people are reluctant to publish their mistakes, but I believe it can be a useful learning tool. I will keep most of the text exactly as originally published but I've changed some of the naming to make it clear that it should not be used. I'll add some additional comments at the end.
Original Posting:
Our second version does things a little differently. We'll reuse a lot of the Composables from the first so you will need to add those yourself if you are only creating version 2.
LocalNavigate (for GuardedNavigate)
object BasicNav2OldBuggy {
private val LocalNavigate = staticCompositionLocalOf<(NavigationScreen)
-> Unit> { error("No LocalNavigate provided") }
...
- The value rarely changes
- When everything below it will change when it changes
@Composable
private fun HomeScreen() {
val navigate = LocalNavigate.current
BasicNav1.HomeScreen { navigate(NavigationScreen.List) }
}
@Composable
private fun ListScreen() {
val navigate = LocalNavigate.current
BasicNav1.ListScreen({
index -> navigate(NavigationScreen.Detail(index))
})
}
GuardedNavigate (uses LocalNavigate)
- GuardedNavigate takes in a navigation target and screen to display.
- lifecycle is used to track when we drop from RESUMED (not animating so fully interactive in the forground) to STARTED (animating a transition) and is monitored by our LaunchedEffect to update hasNavigated.
- hasNavigated is our guard. It returns true during a navigational transition animation.
- currentOnNavigate by rememberUpdatedState tracks our most recent navigation action so we can track updates. Without this, our guardedNavLambda would only take the first value passed into the function and fail to update for future actions.
- LaunchedEffect monitors our lifecycle to keep hasNavigated updated.
- Within the LaunchedEffect, we use snapshotFlow to convert our compose state into a Flow so we can observe lifecycle changes inside the coroutine.
- guardedNavLambda prevents us stacking clicks and making a mess of our backstack.
- CompositionLocalProvider injects the initial value to LocalNavigate that allows us to navigate without having to pass lambdas everywhere.
@Composable
private fun GuardedNavigateOldBuggy(
onNavigate: (NavigationScreen) -> Unit,
content: @Composable () -> Unit
) {
val lifecycle =
LocalLifecycleOwner.current.lifecycle
var hasNavigated by remember {
mutableStateOf(false)
}
val currentOnNavigate by
rememberUpdatedState(onNavigate)
LaunchedEffect(lifecycle) {
snapshotFlow { lifecycle.currentState }
.collect {
if (it == Lifecycle.State.RESUMED)
hasNavigated = false
}
}
val guardedNavLambda =
remember {
{ screen: NavigationScreen ->
if (!hasNavigated) {
hasNavigated = true
currentOnNavigate(screen)
}
}
}
CompositionLocalProvider(
LocalNavigate provides guardedNavLambda,
content = content
)
}
NavigationApp
We also need to adjust our NavigationApp to use our GuardedNavigate. This is quite simple. We are now passing the actions to GuardedNavigate and not the screen Composables.
Note: We are using GuardedNavigate on our detail screen. We don't actually need it here as we cannot navigate anywhere deeper from there. I have left that for consistency and to allow for deeper navigation if required.
NavDisplay(
backStack = backStack,
onBack = { backStack.removeLastOrNull() },
entryProvider = entryProvider {
entry<NavigationScreen.Home> {
GuardedNavigateOldBuggy(
{ backStack.add(it) }
) { HomeScreen() }
}
entry<NavigationScreen.List> {
GuardedNavigateOldBuggy(
{ backStack.add(it) }
) { ListScreen() }
}
entry<NavigationScreen.Detail> { key ->
GuardedNavigateOldBuggy(
{ backStack.add(it) }
) { BasicNav1.DetailScreen(key.id) }
}
}
)
Result
I thought this was good, but it actually contains another bug! If you navigated into a list item and then pressed back during the transition animation you would not be able to choose any other options in the list. The only choice was to jump back out to the home screen then head back in again...
The same thing can also happen on the main screen. If that happens there, you have to kill the app and then relaunch it for it to be functional.
As well as the bugs, I wasn't really very happy with the default transition animation as it feels a little slow and can look ugly when navigating two pages quickly. We'll fix that as well.
I missed this when I was first published so I quickly unpublished the blog and started working on a solution. It's already working and testing so I will hopefully post that later today.
Comments / Thoughts
Links
My links
GitHub Gists
Related Blog Posts
- Navigation 3 Part 1 - (will be reposted soon)
- Navigation 3 Part 2 - (will be posted soon)
Official links
Google:
- Navigation3
- Nav3 Recipes (GitHub)
- Nav3 Recipes (GitHub) - Passing ViewModels
- Backstack
- CompositionLocal
- ViewModel
- StateFlow and SharedFlow
- rememberUpdatedState
- snapshotFlow
- Lifecycle
- Recomposition


Comments
Post a Comment