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:
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.
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.
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:
For this example, I'll have HomeFragment
as the start destination; and FirstFragment
, SecondFragment
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 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:
We navigate to the FirstFragment
and then to the SecondFragment
:
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 SecondFragment
:
Let's start hitting the back button, a new FirstFragment
is pushed onto the back stack:
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:
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:
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.