Creating a BottomNavigation Multi-Stack using Child Fragments with Jetpack Navigation

Gabor Varadi
3 min readFeb 26, 2021

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!

Conclusion

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.

Apparently, 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.

--

--

Gabor Varadi

Android dev. Zhuinden, or EpicPandaForce @ SO. Extension function fan #Kotlin, dislikes multiple Activities/Fragment backstack.