Assume we are scrolling some shopping list and we open a specific item. While checking details we realize that we probably have something similar in our cart already, so we would like to change tab from listing to cart and check that out. For that, we would like bottom nav to save our progress, so when we come back to a listing tab, we are not starting from the beginning. In basic Android Compose Navigation we don't have that functionality. Let's create our own solution!

Section 1 - Basic bottom navigation

As the very base of our app we will have 2 bottom nav tabs: Home and Settings. Each of them will have 2 screens: Dashboard and Inner. We need at least two of each to properly test our scenario.

  
  

Besides that, we need 2 screens alike but with just a different name Home -> Settings.

Now we need Destination tree for our routing, so we will know where we can navigate to. It's a nice practice to create separate class for it so we have everything structured.

  

Next, we have to create NavigationBar, so we can inject it into our main application Scaffold that will contain our app's NavHost.

  
  

Last, but not least, we create NavGraphs for both home and settings screens. Here as well I will provide NavGraph for only Home destination to not create unnecessary snippets and keep article cleaner.

  

Now all we need to do is to invoke our Navigation function in the MainActivity.

  

Preview of default bottom bar navigation behavior

Section 2 - Upgraded BottomBar

We have our base navigation, now we can start upgrading it.

This part of our bottom bar upgrade requires a little bit explanation. We are gonna have multiple NavHostController instances. One for every bottom bar tab. For that, we need some way of differentiating them. The easiest way is just to use simple enum, but befor we create it, we should think of a way of using those controllers in a NavHost, right? Yes, and no. Because 1 NavHost = 1 NavHostController, so we cannot he multiple controllers in there.

If we cannot use more than one controller in one host, we have to create multiple hosts then. Learn how to do it!

Screenshot showing Multiple NavHosts with NavHostControllers.
Multiple NavHosts with NavHostControllers

That one is actually pretty simple. We can achieve that using HorizontalPager. Pager will provide us lazy list behvaior, so we will be able to create nav host controller for each of our bottom destination. So, for some graphic visualization, let's change our Navigation component a bit.

  

For now, it will not compile and IDE will show you few errors, but don't worry, we are gonna resolve them step by step. At first, we need previously mentioned BottomDestination.

  

With that, we can not create our new BottomBar class. It's main purpose will be to handle switching between bottom navigation and every other navigation functionality that will come to our mind. But what will such class need to work? For sure PagerState to switch pages and probably NavHostControllers, so we are gonna have specific instance at hock in one place, but with some way of differentiating it by our bottom destinations so they are not complete strangers to us.

  

Nice! We are doing great progress here! But it does not do anything at all...let's change that! What core purpose we would like that class to serve? What do you think about something like that:

Switching pages:

  

Returning nav controller based on bottom destination:

  

Creating bottom nav bar component that will be displayed on the screen:

  

I think that's pretty decent functionalities. Now lets try and put them together.

  

If you want to now more about @Stable annotation used for our new BottomBar class check official android documentation.

In step 3 we introduced new component, BottomNavigation. This is gonna be our new BottomBar from previous section.

  

As you can see instead of navController, we have onClick as we are gonna change our bottom tab based on BottomDestinations index.

Now that we have all components that we were missing in our class, we can think of how should we create our bottom bar. Sure, we could simply create it just like that val bottomBar = BottomBar(...), but would that really by sufficient? Yea, not so much. Besides, it will be much cleaner to have constructor like method. So, let's quote our bottom bar so we will have clean look at it.

  

Last but not least, summarize it. We need pager state, that will be easy, we just need to use rememberPagerState(). What about our map? That's simple as well, we just gonna loop for our BottomDestination values and for each use rememberNavController().

  

There we go! As as addition I've added returning our BottomBar instance as remember, so it will not change in any recomposition that is gonna occur.

Please note: rememberPagerState is experimental API and can change in the future.

At this point, we have everything created, we already used it in our Navigation, so let's run app! But wait, there is still some error...ouh! We forgot about an important thing, obtaining route for each NavHost as startDestination. This is gonna be similar to getting BottomDestination by page, just add this code before last } in Destination class.

  

Upgraded bottom bar navigation behavior with switching tabs

Section 3 - Additional functionalities

This section will contain 2 additional functionalities.

1. Going back to tab's initial view

Functionality that I personally like the most is going back to root on currently chosen tab click. Right now, clicking on selected tab does not change anything. But with just a little bit of effort, we can have it. Lets see:

  

First, add this method to the BottomBar class, so we can use it anywhere we like. Then modify BottomNavigation a bit.

  

The only thing that have changed was adding onCurrentSelectionClick as an argument and it's usage in onClick of NavigationBarItem.

  

Lastly, we have to add proper method while creating nav component and voila! We can happily use our new functionality!

Changing tab to the root one in upgraded bottom bar

2. Changing bottom bar visibility

It'd be pretty cool to be able to change bottom navigation's visibiity, right? For that, we will need support variable. You can put it right at the beginning of out BottomBar class.

  

Then, we're gonna need some function that will handle change of visibility and we will have to make BottomNavigation aware of new variable. So, as follows:

  
  

Of course we have to add isActive = isBottomBarVisible.value to the creation of BottomNavigation instance in createNavComponent function.

Last to add is invokation of this function. Let's say, we do not want to see our bottom navigation in settings inner screen. For that, we have to find our composable for it and simply invoke our new function.

  

Changing bottom bar's visibility in upgraded bottom bar

Summary

And thus we created saveable bottom bar navigation that will serve as well in our Compose world. What's even better about it (I didn't try it out, just assuming, but that's next thing to check!), is that this solution probably works with Compose Multiplatform as well! Probably with few changes for specific platform, but still, it should work.

Versions used in this article:

  • kotlin - 1.9.10
  • ComposeBom - 2023.10.01
  • androidx.navigation:navigation-compose:2.7.5
  • androidx.hilt:hilt-navigation-compose:1.1.0
  • androidx.core:core-ktx:1.12.0
  • androidx.lifecycle:lifecycle-runtime-ktx:2.6.2
  • androidx.lifecycle:lifecycle-runtime-compose:2.6.2
  • androidx.activity:activity-compose:1.8.0