I'm building a Jetpack Compose app that uses a bottom NavigationBar to switch between destinations. Each destination is implemented as a nested graph, and there's also a shared TopAppBar. I'm following the type-safe navigation approach shown in this official video.
Desired navigation schema:
├── AGraph
│ ├── A1Screen
│ └── A2Screen
└── BGraph
├── B1Screen
└── B2Screen
Demonstration code:
@Serializable
object AGraph
@Serializable
object A1
@Serializable
object A2
@Serializable
object BGraph
@Serializable
object B1
@Serializable
object B2
data class NavBarDestination<T : Any>(
val route: T,
val icon: ImageVector,
val label: String,
)
val navBarDestinations = listOf(
NavBarDestination(
route = AGraph,
icon = Icons.Default.Call,
label = "A",
),
NavBarDestination(
route = BGraph,
icon = Icons.Default.Settings,
label = "B",
),
)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun OctopusApp() {
val navController = rememberNavController()
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
Scaffold(
topBar = {
TopAppBar(
title = {
Text(stringResource(R.string.app_name))
},
navigationIcon = {
val canNavigateUp = navController.previousBackStackEntry != null
if (canNavigateUp) {
IconButton(onClick = navController::navigateUp) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(R.string.up_button),
)
}
}
},
)
},
bottomBar = {
NavigationBar {
navBarDestinations.forEach { destination ->
NavigationBarItem(
selected = currentDestination?.hierarchy?.any {
it.hasRoute(destination.route::class)
} == true,
onClick = {
navController.navigate(destination.route) {
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
},
icon = {
Icon(
imageVector = destination.icon,
contentDescription = destination.label,
)
},
label = { Text(destination.label) },
)
}
}
},
) { innerPadding ->
NavHost(
navController,
startDestination = AGraph,
modifier = Modifier
.fillMaxSize()
.padding(innerPadding),
) {
navigation<AGraph>(startDestination = A1) {
composable<A1> {
A1Screen(onClick = { navController.navigate(A2) })
}
composable<A2> {
A2Screen()
}
}
navigation<BGraph>(startDestination = B1) {
composable<B1> {
B1Screen(onClick = { navController.navigate(B2) })
}
composable<B2> {
B2Screen()
}
}
}
}
}
Problem
When I tap between items in the bottom navigation bar, the back stack is actually grows by exactly one destination (even with that popUpTo action in NavigationBarItem onClick callback). As a result, the case of navController.previousBackStackEntry != null is fired and the Up button appears in the TopAppBar, which is not the desired behavior for primary destinations.
What I want instead:
TopAppBar should not show the Up button when switching top level tabs — unless deep inside a section.
Is there a canonical way to implement this behavior? Do I really need to implement custom multiple back stacks (then why do we need these nested navigation graphs at all?)?
Update the argument of the popUpTo() function in your NavigationBarItem's onClick parameter:
navController.navigate(destination.route) {
popUpTo(0) {
saveState = true
}
launchSingleTop = true
restoreState = true
}