androidnavigationandroid-jetpack-compose

Jetpack Compose nested graphs navigation and TopAppBar Up button behavior


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


Solution

  • 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 
    }