Building an artificial back stack with the Android Navigation component

Building an artificial back stack with the Android Navigation component

Context

Backwards navigation with Navigation Component isn't a mystery, is it? We don't have to do anything, we have it for free.

But, what if you want to navigate straight to a specific destination? And be able to navigate back to the start destination? Like if you had navigated manually to that specific destination.

I expected that the backwards navigation continues working but it doesn't. And this is the reason that this article exists.

Note: deep linking provides this behaviour. It simulates manual navigation and creates a synthetic back stack.

Understanding the back stack

The first thing is to understand what the back stack is and how it works with the Navigation component. The back stack is a LIFO stack that stores the activity and its fragments. The last fragment pushed onto the back stack will be the first fragment popped off the stack when we hit the back button.

For example, if we have the following navigation:

Fragment A -> Fragment B -> Fragment C

Its back stack will look like this:

Navigation Component.drawio.png

If we hit the back button, the fragment on the top is popped off the stack and now Fragment B is in the foreground:

image.png

Hit the back button again and Fragment B is popped off the stack:

image.png

Finally, if we hit the back button one more time, Fragment A and the activity are popped off the stack and we exit the app:

image.png

This backwards navigation behaviour comes for free, we don't have to do anything.

Problem

The described behaviour works as long as we navigate manually to a destination. Yet, what if we want to navigate to a specific destination skipping destinations in between? And be able to navigate back to the start destination?

You can think of a sign-up flow for example. You may want to let the user leave the flow at any point. And allow him to rejoin the flow at the last point he left (and don't lose a user because of a long sign-up process).

In this case, the backwards navigation isn't what we expect. If we navigate straight to Fragment C and hit the back button, we won't see Fragment B. We'll see Fragment A. What is happening is that Fragment B wasn't pushed onto the back stack because we never navigated to it.

Navigation Component.drawio (5).png

Solution

To amend this problem, the solution I've found is that we have to build the back navigation ourselves.

Show me the code

You can find the code here (artificial-back-stack branch).

The aim is to have a navigation graph where all the destinations are connected forwards and backwards:

NavGraph

For this example, I'll have HomeFragment as the start destination; and FirstFragment, SecondFragment and, ThirdFragment as destinations.

The graph will look like this:

image.png

Once we have the forwards navigation, we have to build the backwards navigation. Thus, when we navigate straight to a destination, we're still able to navigate back to the start destination. For that, we have to add the actions for the backwards navigation:

We also have to provide custom back navigation to our fragments and specify the action. For example, this is how we'd provide custom back navigation to FirstFragment:

Everything seems to work fine. We can navigate through the navigation graph forwards and backwards. However, when we navigate back to the HomeFragment, we'll notice a bug. We enter a loop between the HomeFragment and the FirstFragment and can't exit the app:

What is happening?

Let's analyse the back stack to understand what is happening. The back stack when we launch the app:

image.png

We navigate to the FirstFragment and then to the SecondFragment:

image.png

When we hit the back button, because of the action_secondFragment_to_firstFragment action, a new destination (FirstFragment) is added:

image.png

Hit the back button again and, because of the action_firstFragment_to_homeFragment action, a new HomeFragment is added:

image.png

Back button again and the HomeFragment is popped off the stack (FirstFragment is visible again). Hit back again and a new HomeFragment is added, and so on:

image.png

How to fix the loop bug

We have to specify the app:popUpTo attribute in the action_firstFragment_to_homeFragment action. By including app:popUpTo="@id/homeFragment", we're telling the navigation graph that all the destinations are popped off the stack until reaching the specified destination (HomeFragment). With this attribute, we've fixed the loop bug.

However, there is a new bug. We need to hit the back button twice when we're in the HomeFragment to exit the app:

Let's analyse the back stack again to understand why this new bug is happening. The back stack when we navigate to the FirstFragment and then to the SecondFragment:

image.png

Let's start hitting the back button, a new FirstFragment is pushed onto the back stack:

image.png

Because of app:popUpTo="@id/homeFragment", when we hit the back button again, all the fragments are popped off the stack until reaching the destination HomeFragment. And a new HomeFragment is pushed onto the back stack:

image.png

Now we have two HomeFragment instances and this is the reason why we need to hit the back button twice to exit the app.

How to fix this new bug

The app:popUpToInclusive attribute fixes the bug:

This attribute pops the target off the stack as well:

image.png

Now we only have one HomeFragment in the back stack and hitting the back button once will exit the app:

You can learn more about app:popUpTo and app:popUpToInclusive here (skip to the Pop additional destinations off the back stack section).

That's all

I don't know if there is a better way of achieving this. I couldn't find anything that solves this problem. After investigation, this is the solution I came up with.