Creating a BottomNavigation Multi-Stack using Child Fragments with Jetpack Navigation
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?
Each framework has its own unique ways. For example, with Jetpack Navigation you might use the NavigationExtensions, which requires you to host the BottomNavigationView at the Activity-level.
But how would you do it with Jetpack Navigation, in such a way that the BottomNavigationView is confined to a single child fragment, and that child fragment would host all tabs that each have a NavController (backstack) available to them?
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 Jetpack Navigation 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.
We will use this knowledge later in the article.
What Jetpack Navigation normally does
When you use Jetpack Navigation (using its XML-based DSL) you typically create a
navigation.xml (typically 1 per compilation module, but multiple nav graphs + include can be used too), which is then given to a
NavHostFragment in a
FragmentContainerView. This is what allows the
NavController to be initialized with a graph, and save/restore its state.
Creating a NavController and backstack for each bottom navigation tab
To actually have a bottom navigation view, that manages 3 fragments, that each hold a navigation stack, we need to make a
NavHostFragment for each tab, and initialize them with their own graphs.
To make that happen, we need to create the graph per each tab with their own initial destination, and we must create the “fragment host” fragment that would be able to show child fragments based on a graph supplied as an argument, using Jetpack Navigation and NavHostFragment.
Then we can create the fragment that will host the 3 tabs, setting up which graph to show on which tab as an argument.
Navigating within a child fragment in a child stack
To navigate within a given child stack, we can rely on the parent fragment (which hosts the
NavController) of the child fragments. Then we can use
<actions within the child graph as you normally would.
And to intercept back events, we can rely on the
OnBackPressedDispatcher, which is the root Activity. However, thanks to the lifecycle-aware registrations, this will only be triggered when the Fragment is in fact started.
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 NavHostFragments that each host a NavController of their own.
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 — using Jetpack’s combined additions: Navigation, Lifecycle, and OnBackPressedDispatcher — that allowed fairly simple lifecycle integration even at a nested level.
NavHostFragment is quite powerful, due to how it can be placed at any level of the XML layout hierarchy — and still be connected to the lifecycle, the saved state registry, the back press dispatcher, and the right viewmodel store!
It might even raise the bar for how simple it should be to create and integrate a backstack at the level of a nested child, rather than one that is globally available to all? Still, until having to manually dispatch deep-links and selecting the tab to make that happen, this solution should work quite nicely for when the bottom navigation should not be app-global.
The sample code shown throughout the article is available here.