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?)?