Reputation: 1010
I'm using BottomNavigation
in Jetpack Compose with navigation-compose:2.5.0-alpha04
and I want to encapsulate each tab's flow with nested navigation. To achieve this I created a single NavHost with different graphs inside:
NavHost(
navController = navController,
startDestination = defaultTab.route
) {
eventsGraph()
employeesGraph()
devicesGraph()
feedbackGraph()
}
Each graph contains its own composable
s inside a navigation
extension function.
As docs suggest, when BottomNavigationItem
is clicked, I'm configuring my NavOptions
the following way:
onClick = {
navController.navigate(tab.route) {
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
// We want to reset the graph if it is clicked while already selected
restoreState = tab != currentTab
}
currentTab = tab
}
I'm expecting this code to ignore clicks on an already selected item and always having any selected tab as nav graph's root, i.e. quitting the app on back press if user is at the root destination of any given tab.
Instead, every click on a default tab at its start destination puts another instance of the same destination in the stack, and back press at a non-default tab puts me back to the defaultTab
's destination.
I tried popping destinations inclusive
, but NavController
unsurprisingly just can't find saved Destination
's ID to restore state for. I tried to saveState
by the same tab != currentTab
condition by which I restore it, but it appears to have no effect.
Is it possible to achieve desired behavior by the means of Navigation APIs or do I need to handle this by myself?
I eventually came up with the following solution for resetting the current tab's graph. Basically, instead of navigating with popUpTo
option we're just popping the stack to the start destination.
onClick = {
if (tab == currentTab) {
navController.popBackStack(
route = tab.startDestination,
inclusive = false
)
return@BottomNavigationItem
}
/* ... */
}
There is still an issue with defaultTab
's graph being the root, which means pressing the back button on any non-defalut tab's start does not exit the app.
Upvotes: 2
Views: 8148
Reputation: 1010
Short answer: yes, you need to write this logic by yourself. Fortunately, most of it is handled by the navigation library.
What was required:
Every one of those except 4 was achieved. As ianhanniballake pointed out, exiting the app on system back press must always happen in the start destination of the app, so I left the default behavior in place.
As for items 1-3, here's complete onClick
implementation for BottomNavigationItem
:
onClick = {
// Handling resetting current tab's state
if (tab == currentTab) {
navController.popBackStack(
route = tab.startDestination,
inclusive = false
)
return@BottomNavigationItem
}
// Handling singleTop behavior with saving state
navController.navigate(tab.route) {
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = tab != currentTab
}
appBarViewModel.setCurrentTab(tab)
}
What is tricky about this is currentTab
from the previous implementation is not updated when defaultTab
is shown after system back press, so I am updating it based on current nav back stack as a side effect:
val navBackStackEntry by navController.currentBackStackEntryAsState()
LaunchedEffect(navBackStackEntry) {
// This condition is possible to be true if a system "Back" press was detected in a non-default tab,
// navigating the user to app's start destination.
// To correctly reflect that in bottom navigation, this code is needed
if (navBackStackEntry?.destination?.route == appBarViewModel.defaultTab.startDestination)
appBarViewModel.setCurrentTab(appBarViewModel.defaultTab)
}
Upvotes: 2