Building an artificial back stack with the Android Navigation component
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:
If we hit the back button, the fragment on the top is popped off the stack and now Fragment B is in the foreground:
Hit the back button again and Fragment B is popped off the stack:
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:
This backwards navigation behaviour comes for free, we don't have to do anything.
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.
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 (
The aim is to have a navigation graph where all the destinations are connected forwards and backwards:
For this example, I'll have
HomeFragment as the start destination; and
ThirdFragment as destinations.
The graph will look like this:
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
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:
We navigate to the
FirstFragment and then to the
When we hit the back button, because of the
action_secondFragment_to_firstFragment action, a new destination (
FirstFragment) is added:
Hit the back button again and, because of the
action_firstFragment_to_homeFragment action, a new
HomeFragment is added:
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:
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
Let's start hitting the back button, a new
FirstFragment is pushed onto the back stack:
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:
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
app:popUpToInclusive attribute fixes the bug:
This attribute pops the target off the stack as well:
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:popUpToInclusive here (skip to the Pop additional destinations off the back stack section).
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.