Navigation and the back stack

The NavController holds a "back stack" that contains the destinations the user has visited. As the user navigates to screens throughout your app, the NavController adds and removes destinations to and from the back stack.

In being a stack, the back stack is a "last in, first out" data structure. The NavController therefore pushes items to and pops items from the top of the stack.

Basic behavior

These are the core facts you should consider regarding the behavior of the back stack:

  • First destination: When the user opens the app, the NavController pushes the first destination to the top of the back stack.
  • Pushing to the stack: Each call NavController.navigate() pushes the given destination to the top of the stack.
  • Popping top destination: Tapping Up or Back calls the NavController.navigateUp() and NavController.popBackStack() methods, respectively. They pop the top destination off the stack. See the Principles of Navigation page for more information about the difference between Up and Back.

Pop back

The NavController.popBackStack() method attempts to pop the current destination off the back stack and navigate to the previous destination. This effectively moves the user back one step in their navigation history. It returns a boolean indicating whether it successfully popped back to the destination.

Pop back to a particular destination

You can also use popBackStack() to navigate to a particular destination. To do so, use one of its overloads. There are several that allow you to pass in an identifier, such as an integer id or a string route. These overloads take the user to the destination associated with the given identifier. Critically, they pop everything on the stack above that destination.

These overloads also take an inclusive boolean. It determines whether the NavController should also pop the specified destination off the back stack after having navigated to it.

Consider this brief snippet for an example:

navController.popBackStack(R.id.destinationId, true)

Here the NavController pops back to the destination with the integer id destinationId. As the value of the inclusive argument is true, the NavController also pops the given destination from the back stack.

Handle a failed pop back

When the popBackStack() returns false, a subsequent call to NavController.getCurrentDestination() returns null. This means the app has popped the last destination off the back stack. In this case, the user sees only a blank screen.

This can occur in the following cases:

  • popBackStack() did not pop anything from the stack.
  • popBackStack() did pop a destination off the back stack and the stack is now empty.

To resolve this, you must then navigate to a new destination or call finish() on your activity to end it. The following snippet demonstrates this:

kotlin

...

if (!navController.popBackStack()) {
    // Call finish() on your Activity
    finish()
}

java

...

if (!navController.popBackStack()) {
    // Call finish() on your Activity
    finish();
}

Pop up to a destination

To remove destinations from the back stack when navigating from one destination to another, add a popUpTo() argument to the associated navigate() function call. popUpTo() instructs the Navigation library to remove some destinations from the back stack as part of the call to navigate(). The parameter value is the identifier of a destination on the back stack. The identifier can be an integer id or string route.

You can include an argument for the inclusive parameter with a value of true to indicate that the destination you have specified in popUpTo() should also pop off back stack.

To implement this programmatically, pass popUpTo() to navigate() as part of NavOptions with inclusive set to true. This works in both Compose and Views.

Save state when popping up

When you use popUpTo to navigate to a destination, you can optionally save the back stack and the states of all destinations popped off the back stack. You can then restore the back stack and destinations when navigating to that destination at a later time. This lets you preserve state for a given destination and have multiple back stacks.

To do this programmatically, specify saveState = true when adding popUpTo to your navigation options.

You can also specify restoreState = true in your navigation options to automatically restore the back stack and the state associated with the destination.

For example:

navController.navigate(
    route = route,
    navOptions =  navOptions {
        popUpTo<A>{ saveState = true }
        restoreState = true
    }
)

To enable saving and restoring state in XML, define popUpToSaveState as true and restoreState as true respectively in the associated action.

XML example

Here is an example of popUpTo in XML, using an action:

<action
  android:id="@+id/action_a_to_b"
  app:destination="@id/b"
  app:popUpTo="@+id/a"
  app:popUpToInclusive="true"
  app:restoreState=”true”
  app:popUpToSaveState="true"/>

Compose example

The following is a complete example of the same in Compose:

@Composable
fun MyAppNavHost(
    modifier: Modifier = Modifier,
    navController: NavHostController = rememberNavController(),
    startDestination: Any = A
) {
    NavHost(
        modifier = modifier,
        navController = navController,
        startDestination = startDestination
    ) {
        composable<A> {
            DestinationA(
                onNavigateToB = {
                // Pop everything up to, and including, the A destination off
                // the back stack, saving the back stack and the state of its
                // destinations.
                // Then restore any previous back stack state associated with
                // the B destination.
                // Finally navigate to the B destination.
                    navController.navigate(route = B) {
                        popUpTo<A> {
                            inclusive = true
                            saveState = true
                        }
                        restoreState = true
                    }
                },
            )
        }
        composable<B> { DestinationB(/* ... */) }
    }
}

@Composable
fun DestinationA(onNavigateToB: () -> Unit) {
    Button(onClick = onNavigateToB) {
        Text("Go to A")
    }
}

More granularly, you can change how you call NavController.navigate() in the following ways:

// Pop everything up to the destination_a destination off the back stack before
// navigating to the "destination_b" destination
navController.navigate("destination_b") {
    popUpTo("destination_a")
}

// Pop everything up to and including the "destination_a" destination off
// the back stack before navigating to the "destination_b" destination
navController.navigate("destination_b") {
    popUpTo("destination_a") { inclusive = true }
}

// Navigate to the "search” destination only if we’re not already on
// the "search" destination, avoiding multiple copies on the top of the
// back stack
navController.navigate("search") {
    launchSingleTop = true
}

For general information about passing options to NavController.navigate(), see the Navigate with options guide.

Pop using actions

When navigating using an action, you can optionally pop additional destinations off of the back stack. For example, if your app has an initial login flow, once a user has logged in, you should pop all of the login-related destinations off of the back stack so that the Back button doesn't take users back into the login flow.

Additional reading

For more information, read the following pages:

  • Circular navigation: Learn how you can avoid an overstuffed back stack in cases where navigation flows are circular.
  • Dialog destinations: Read about how dialog destinations introduce unique considerations to how you manage your back stack.