It’s been a recurring question, regardless of navigation framework of choice: how is it possible to create a screen with a bottom navigation view, that would host a navigation history stack per each tab?
How bottom navigation with child fragments normally works
If you intend to create a BottomNavigationView that is hosted by a Fragment (and that Fragment hosts each tab as a child fragment), there’s a bit of work needed to be done. To keep the fragments alive while switching between them, they must all be added. However, to make sure that the fragments are in the correct lifecycle (stopped while not showing), it makes sense to detach them (and attach the one that is meant to be showing).
This example is also available here.
Please note that there is nothing specific to Simple-Stack in this snippet, this is how a fragment can manage its child fragments and keep only one of them attached, while the rest are detached.
In fact, the NavigationAdvancedSample works the same way.
We will use this knowledge later in the article.
What Simple-Stack normally does
When you use Simple-Stack, you typically install a
Navigator.install(). This allows a single global
Backstack instance to be accessible through the Activity’s context (and thus making it available to all views within the Activity), and provides automatic lifecycle management to ensure that navigation is handled correctly.
This is important to know, because if we want to create a
Backstack for each Fragment within our BottomNavigationView, we must manage the lifecycle (and keep the
Backstack instance alive across configuration changes) ourselves.
Thankfully, this is actually quite simple to do! After all, fragments give us all the callbacks we need for the lifecycle management, while ScopedServices can help us keep stuff alive across config changes.
Keeping the Backstack alive for each Fragment
To keep the backstack alive, I’ll introduce something called a
FragmentBackstackHost. It’s really just responsible for retaining the fragment across configuration changes (and also to intercept back presses for the current selected tab).
Managing the lifecycle of nested Backstacks
Now we need to manage the lifecycle of this
Backstack instance from a Fragment:
Registering the stack hosts as a scoped service
Now in order for the backstack fragment to see the stack hosts, we must provide them as a scoped service, so we will register it on our root screen:
Setting up the host fragments (and their child fragments)
Remember the snippet at the beginning, which allows us to select a fragment with a bottom navigation view, and show (attach) 1 fragment, while hiding (detaching) the rest?
That’s exactly what we need to do, just for these fragments that host a backstack each.
Navigating within a child fragment in a child stack
To navigate within a given child stack, all we need to do is use the child stack that was added with the help of the
This might seem a bit tricky, but after all, we are getting a service registered to the top-most parent fragment’s scope, and can identify the stacks with their provided names.
And with that, we’re actually done!
With that, we have created a Fragment, that hosts 3 child Fragments, and each of these 3 child Fragments hosts a Backstack of their own, that survives configuration changes and process death.
Was it tricky? Well, it requires knowledge of the Fragment lifecycle to set up, and knowing about
FragmentTransaction.add, FragmentTransaction.attach, FragmentTransaction.detach. However, this is also the same mechanism we’ve been using for years in ViewPager’s
FragmentPagerAdapter, so surely, it’s not too alien? 😅
Otherwise, for a problem that’s been unresolved for years, it didn’t seem that complicated to implement— with the added ability from Simple-Stack — that allowed Fragments to be capable of easily keeping things alive across configuration changes, and also be able to intercept back with an intuitive API.