Proper back stack on Android, every time

Daniel Voko
The Startup

--

When navigating in a mobile app, the screens opened after one another form a stack, the back stack. Just like pages in a book, moving forward and backward in a back stack feels natural and doesn’t require conscious thinking. There are some minor differences in the way the two dominating platforms and even particular apps build and handle their back stacks, but we are used to have a back stack of some kind and this is why it is so disturbing when we don’t get one.

This can happen when the app is started by another app or is launched from a deep link or notification, or any other way when the app is not started at the "Start destination" — the first screen that is presented when starting the app fresh.

In the case of launching the app from the launcher, building the back stack doesn’t require any work, the stack is building itself. But starting the app from a link or from a notification presents a whole new situation. In these cases the app is rarely needed to start from the "Start destination", but from different destinations that are deep in the stack usually. Such application open events replace whatever current back stack the app has at that moment, with a synthetic back stack as detailed in the Principles of navigation.

The Android Navigation Component supports deep linking and building synthetic back stacks, but to achieve the often desired result that is that

"it should match a back stack that could have been achieved by organically navigating through the app."

may need some setup.

Let's try it out with a simple application consisting of three fragments (Root, Level1 and Level2) that can be opened sequentially. The navigation graph looks like this:

the original navigation graph

The app is deep link aware with a few lines of code in two files. AndroidManifest:

And the navigation graph:

If we try it out with the link

https://properbackstack.com/level1

it all works fine. The app is opened with Level1 fragment on top, and navigating back one time gets the user to the Root fragment.

But if we try it with

https://properbackstack.com/level1/level2

it is not as expected. The app is opened with Level2 fragment on top all right, but navigating back one time gets the user — again — to fragment Root. Fragment Levell is omitted. Why?

If we open up the NavDeepLinkBuilder class reference, it is seen that:

When this deep link is triggered:

The task is cleared.

The destination and all of its parents will be on the back stack.

Calling NavController.navigateUp() will navigate to the parent of the destination.

Then why is Level2 fragment not on the back stack? The answer is in the second sentence:

The parent of the destination is the start destination of the containing navigation graph.

By the default behavior, only the start destination of the navigation graph is considered a parent, and fragment Level2 is not the start destination. But then how to build the natural back stack of the app? The deep linking guide might have a suggestion:

When a user opens your app via an explicit deep link, the task back stack is cleared and replaced with the deep link destination. When nesting graphs, the start destination from each level of nesting — that is, the start destination from each <navigation> element in the hierarchy—is also added to the stack.

This means that nesting navigation graphs can be the solution. If the navigation graph of the sample application is refactored so that there is a root navigation graph with the root fragment as start destination and a nested graph as the second destination, and in the nested graph, the start destination is the Level1 fragment and the second destination is the Level2 fragment, the deep link back stack will be the same as the original back stack.

graphical representation of the refactored navigation graph

So far it is easy and straightforward, but deep links rarely are static links like the ones above, and the case is a bit more complicated if they contain path and query parameters. I won't explain deep link parameters here, the documentation does a good job on that, we only need to know that to build proper synthetic back stacks, parameters should be translated to fragment arguments.

But there is a catch: the parameters have to contain all the arguments of every fragment on the back stack (or, to turn it around, the parameters must suffice as arguments for every fragment) in order for the fragments on the back stack to be able to load their content when the user navigates back.

If the app shall open the link

https://properbackstack.com/level1/{number}

in fragment Level1 and

https://properbackstack.com/level2/{word}

in fragment Level2, proper back stack cannot be achieved because when opening the second link and navigating back from fragment Level2 to fragment Level1, an IllegalArgumentException is thrown by fragment Level1, missing the argument: number. Fragment Level2 does not need number argument by itself, but have to handle it for building proper back stack. The level 2 link will have to contain both arguments, and look something like this:

https://properbackstack.com/level2/{number}/{word}

One more thing: remember, the navigation graph is nested and the nested graph's start destination is fragment Level1. And as fragment Level1 needs an argument to start, so does the nested navigation graph. The final navigation graph will look like this:

This solution works similarly with notifications as well, where the NavDeepLinkBuilder class can be utilized to inject the arguments and select the navigation graph and destination.

The solution above might not be a universal silver bullet for all the synthetic back stack related issues and might not even solve your app's particular navigation needs, but I hope you found it useful. Let me know if you did :)

The sample application can be found here:

https://github.com/vokod/ProperBackStack/tree/notification

The three branches explain the three topic discussed:

static deep link back stack,

dynamic deep link back stack,

notification back stack.

--

--