Using popBackStack()? Then Your App Likely Has a Bug.

by | Oct 19, 2024

Compose Navigation – The Basics

Your app maintains what is called a back stack. A back stack is simply a stack of screens (navigation destinations) placed on each other (like a stack of pancakes). The deeper the user navigates into your app, going from screen A to screen E, the taller this stack gets. When the user is done at screen E, they want to navigate back. We can do this programmatically by calling popBackStack() when the user taps the back arrow on our screen.

When popBackStack()is called, the topmost screen of the back stack is removed. This is how the user ends up seeing the previous screen again. When calling popBackStack(), this most straightforward behavior can have an unintended side-effect when used. And the cause is not new to us developers.

Compose Navigation Double Back Transition Bug

What happened here? When the back button is tapped, we call popBackStack(), and the app starts a transition animation. This default short animation of a few hundred milliseconds gives the user enough time to tap the back button twice. This also triggers our popBackStack() call twice and pops out backstack twice.

Each call to popBackStack() will pop the current screen, no matter where the user is in the back stack. It is unaware of the current back stack and will always try to pop the topmost screen each time it is called, even if it is the last screen in the back stack. In that scenario, the entire NavHost gets popped; when that happens, it will no longer be in the Compose hierarchy. There is no way to recover it but by restarting the app.

How do we go about solving this?

Preventing The Double Back Navigation Behaviour?

The Simple Solution (That Works In Most Cases)

The easiest fix is to replace the popBackStack() call with navigateUp().

This works because of this small difference between the two functions: navigateUp() is aware of the back stack. Thus, it only pops the screen currently at the top of the stack. If this function is called twice, the user would still go back on screen because the same screen was at the top of the stack when both calls to navigateUp() were made.

Another key difference between popBackStack() and navigateUp() is how they handle going back from a screen opened from a deep link. When a different app opens your app with one of its deep links, navigateUp() will leave your app and return to the app that opened your app. popBackStack() will attempt to move back in your app’s back stack.

An Alternative Solution

Let’s think about what is actually causing this navigation problem: a window of opportunity for the user to tap a button twice. We may have other places in our app other than navigation that we do not want to be triggered twice due to this double-tap scenario. By changing how we look at the problem, we now have a new problem to solve: the double-tap scenario.

You can disable the button temporarily when it is tapped. With this approach, make sure to keep the UI looking nice and responsive. You don’t want the user to be stuck with a button that is disabled without a reason. Here’s a sample implementation for such a back button:

@Composable
fun BackIconButton(
    modifier: Modifier = Modifier,
    delay: Long = 300L, // Default transition time
    onClick: () -> Unit
) {
    var enabled by remember {
        mutableStateOf(true)
    }
    val scope = rememberCoroutineScope()

    IconButton(
        onClick = {
            onClick()
            scope.launch {
                enabled = false
                delay(delay)
                enabled = true
            }
        },
        enabled = enabled,
        modifier = modifier
    ) {
        Icon(
            imageVector = Icons.AutoMirrored.Filled.ArrowBack,
            contentDescription = "Go back"
        )
    }
}

Should You No Longer Pop the Back Stack with popBackStack()?

There are still valid reasons to use popBackStack(). When we need to pop multiple screens to reach a specific destination, we can easily do that with popBackStack(), whereas with navigateUp(), it will always go back one step when called. You can also use popBackStack() when you are sure it can only be called once per transition, which will most likely not be user-initiated.

Conclusion

  1. Your app has its own internal navigation backstack, which is different from the backstack of the device.
  2. popBackStack() can take your user farther back in the backstack than what they intended when it gets triggered multiple times while on the same screen
  3. navigateUp() is a great alternative when your intention is to take the user back only one screen
  4. We also discovered that by preventing multiple back taps, we can apply this to other UI elements in our app that you would not want re-executing due to multiple taps in quick succession.

With a better understanding of how popBackStack() and navigateUp() behave, you can decide which approach best suits your project’s needs. In most scenarios, especially when the user initiates the action, I prefer to use navigateUp() to go back in the backstack.

The Essentials of Industry-Level Android App Development

Master Building Large-Scale Native Android Apps

Take this 23h online course to learn everything it takes to become an industry-ready native Android developer. Learn about multi-module architecture, Gradle, Jetpack Compose, authentication, offline-first development and more.

Read More