What is multiple stacks
When users navigate in the app, they may choose to go back to the previous destination.
The history is kept as a “back stack” by Android.
Sometimes we might need to maintain multiple back stacks in the app and switch between them.
A typical example is for the bottom navigation bar: each tab could have its own back stack, and when switching tabs, the history will be kept.
You may have a different design such as a side drawer, the idea is the same, but for this article, let’s take bottom navigation as the example.
The multi-stack requirements
First thing first, let’s get clear about what we need to achieve.
When the bottom navigation does not support multi-stacks, the behavior is: after switching to another tab and back, all the destinations opened on that tab are gone, only the root destination shown.
That’s also acceptable, and even mentioned as the default behavior for Android platform in material design.
But it also claims that it can be overridden when needed.
If we want to keep whatever user viewed on the previous tab, that’s a multi-stack requirement.
Often, it may not be the only requirement.
Do we want to reset the stack when people double-click the tab?
Do we want to customize the animations?
Do we want to support history between tabs? Like if tab A → B → C, when we are on C’s root screen, do we want to go back to B?
For the bottom navigation’s default behavior (if you create a new bottom nav example using Android Studio), when on the root level of a tab, when clicking the back button, it will first go back to the home tab (the first tab), and when clicking back again, the app will exit.
It applies to the fixed start destination principle.
But if you have special needs, you might also consider how to customize that.
When you go deep dive to the implementations, you will also find details on how to push to the specific stack and how to pop destination out.
Let’s list our requirements here:
- Maintain multiple stacks.
- Switching tabs: by manually clicking tab or some interactions on the other tab. i.e. Dashboard to a specific content tab.
- Push/pop destinations.
- Reselect tab will reset the stack to root level. (clear history.)
- Transition animations.
- Tab history.
Technical Background
It’s important to know what your “destination” is.
For composable or fragments, the solutions could be very different.
For this article’s scope, let’s focus on a traditional android app built using Activity and Fragments.
Fragment lifecycle
The fragment’s lifecycle is quite important to check about, as the Fragment’s lifecycle is related to whether we will need to care about state loss or refreshing action, for example, the re-creation of the ViewModel.
Let’s recap on Fragment’s lifecycle callbacks.
When would Fragment onDestroy called?
- If the
replace
transaction is withoutaddToBackStack()
. - when the fragment is removed or popped out by
popBackStack()
.
When a replace
transaction is called with addToBackStack()
, the old fragment will be pushed into the stack, and the lifecycle is only reached onDestroyView().
When the top one popped out from the back stack, the old fragment’s instance is still the same. The come-to-show fragment’s lifecycle starts with onCreateView()
.
This is what we expected for a single back stack.
If the project is using ViewModel, the Fragment’s ViewModel’s lifecycle is also aligned with the Fragment.
Whenever the Fragment’s onDestroy()
is called, the ViewModel’s onCleared()
is called.
We can keep variables we cared about in the saved instance bundle, or by using SavedStateHandle
. But if the fragment is not destroyed, we get everything for free.
Navigation Libraries/ Possible Solutions
To compare different solutions, I put some samples together.
See my demo: https://github.com/mengdd/bottom-navigation-samples
Jetpack navigation component
official site: https://developer.android.com/guide/navigation
Even in the FragmentManager’s doc it’s recommended to use the navigation library to manage app’s navigation.
The multiple back stack support added in Navigation 2.4.0-alpha01 and Fragment 1.4.0-alpha01.
Tried the demo, it’s quite simple and clean, and the app does not need extra code to get this.
We can check the ideas in this article: https://medium.com/androiddevelopers/multiple-back-stacks-b714d974f134
Pros:
- Most well-known. It’s the official library built by Google.
- Safe args supported.
- NavigationController supports popping to a specific destination.
- Can work together with the Compose navigation library.
Cons:
- Multi-stack support: When switching bottom tabs, the fragments on the previous tab will be destroyed, when navigate back, they will all be re-created. The page may be refreshed again.
- Because each tab should be a nested navigation graph, the xml navigation graph files feels like boilerplate code to maintain, especially for common destinations.
FragmentManager
If we are doing more customization we could also try to build the multi stack solution by using FragmentManager’s new APIs.
As in this doc suggested:
FragmentManager
allows you to support multiple back stacks with thesaveBackStack()
andrestoreBackStack(
methods. These methods allow you to swap between back stacks by saving one back stack and restoring a different one.
Here we can find that it’s what navigation component is using under the hood.
And the why the fragments are all destroyed:
saveBackStack()
works similarly to callingpopBackStack()
with the optionalname
parameter: the specified transaction and all transactions after it on the stack are popped. The difference is thatsaveBackStack()
saves the state of all fragments in the popped transactions.
Pros:
- Fine control. Developers get more control and are aware of what is going on.
- Proper if we are working on a project without any navigation library. We don’t need to migrate a lot of code to a new navigation library.
Cons:
- Lots of fragment transaction boilerplate code.
- Same as navigation components: Fragments on the old tab will be destroyed and re-created.
Enro
https://github.com/isaac-udy/Enro
For large projects with multiple modules, I really recommend this navigation library.
It helps to decouple the modules dependencies.
For multi-stack demo: https://github.com/mengdd/bottom-navigation-samples/tree/main/enro-fragment-multi-stack-sample
Pros:
- Based on annotation, you only need minimal code to do the navigation, making navigation easy like a breeze.
- Designed for multi-module projects, decoupling modules.
- Can get navigation handle in ViewModel.
- Easy to pass type safe arguments and get results.
- Compose Support.
- Having a companion test tool for Unit Test.
Cons:
- Not that well-known. Need to convince others to learn and apply this.
- For Fragments’ multi-stack support: can not reset the stack when reselecting the tab. And it is hard to customize this.
Simple-stack
https://github.com/Zhuinden/simple-stack
I would suggest this article for the multi-stack solution using simple-stack: https://zhuinden.medium.com/creating-a-bottomnavigation-multi-stack-using-child-fragments-with-simple-stack-c73c1ca3bbd4
At the beginning the author showed a demo using child fragments.
This is another idea if we want to implement the multi-stack by ourselves.
After I tried the multi-stack demo provided by simple-stack, I found that actually more code is required to let any fragment gain the instance of each tab’s local stack.
In that way we could push/pop the detail fragment.
Pros:
- The author is quite active in the community, so there are a bunch of speeches and articles for the simple-stack library. Good community support and learning resources.
- Multi-stack support: keep fragments alive.
- Can control or clear the history.
- Compose Support with extensions.
Cons:
- If your bottom tab is in an Activity layout, when using simple-stack’s multi-stack solution, you have to move things into a RootFragment.
- The multi-stack sample provided by the author is quite simple. Need to write some code to retrieve the correct stack instance to push/pop a fragment to/from the current stack.
Other libraries
There are other libraries designed only for multi-stack navigation.
Such as:
- https://github.com/DimaKron/Android-MultiStacks
- https://github.com/JetradarMobile/android-multibackstack
The samples are just along the repos.
Pros:
- Simple solutions with only a few classes. We could even copy them to our codebase to customize.
- The change scope could be limited only to the bottom navigation part.
Cons:
- The libraries are not so well-known, and have the risk of not being maintained anymore.
- Might not be able to work together with other navigation solutions, like Navigation Components. If the project is aiming to apply some nice navigation library in the future, we have to touch this bottom navigation part again.
Conclusions
Possible solutions for android multi-stack navigation:
Solution | Popular | A completed suite of navigation solution | Actively Maintained | Supporting clear stack history | Fragment kept, not destroyed | Multi-modules supported | Compose Extensions |
---|---|---|---|---|---|---|---|
Jetpack Navigation Components | Official, Most Well-known navigation library | Yes | Yes | Yes | No | Yes | Yes |
Fragment Manager | Android SDK | - | Yes | Yes | No | No | - |
Enro | Star: 188 | Yes | Yes | No | Yes | Yes | Yes |
Simple Stack | Star: 1.2k | Yes | Yes | Yes | Yes | Yes | Yes |
Child Fragments | Android SDK | - | Yes | Yes | Yes | No | - |
JetradarMobile/android-multibackstack | Star: 224 | No | No | Yes | No | No | - |
DimaKron/Android-MultiStacks | Star: 32 | No | Not sure | Yes | Yes | No | - |
Note:
- A Completed suite of navigation solution means the solution can be applied to the whole app as a way to navigate, not just to solve the multi-stack navigation. Here I skipped the ways using Android SDK’s fragment manager or child fragments. Of course in your whole app you can depend on them to do all the navigations.
- Fragment kept: means the fragments in stack will not be destroyed when pushing back to the back stack, or switching the bottom tabs. Even though the fragments will be recreated after switching back, we need extra work to cache what state we had before.
Top comments (0)