Android Development

Cross tab navigation in Jetpack Compose

October 4, 2022
Cross tab navigation in Jetpack Compose

The stan­dard android com­pose nav­i­ga­tion seems to work well for basic func­tion­al­i­ty with a NavigationBar, but when I want­ed to nav­i­gate from a screen inside of one NavigationBarItem to anoth­er it all start­ed to unrav­el quick­ly. After sev­er­al hours of debug­ging, test­ing, and retry­ing, I had a sam­ple project, a bug report, and a headache.

So what was the prob­lem? #

Nav­i­gat­ing from one tab” to a route” with­in anoth­er tab was caus­ing hav­oc. In the gif below you can see that by nav­i­gat­ing to Set­tings -> To Home Details, tap­ping on the Set­tings but­ton repeat­ed­ly appears to do nothing.

(Code for this gif avail­able with­in the repo at tag: bug_​ticket_​created)

Well, of course it’s not doing noth­ing, but it sure looks that way. In real­i­ty, it is attempt­ing to nav­i­gate to the route it’s already on. The graph/​navigation has become con­fused and tap­ping Set­tings no results in an attempt­ed nav­i­ga­tion to /home/details, where you just so hap­pen to already be.

So how did I get into this prob­lem? #

In basi­cal­ly every piece of doc­u­men­ta­tion you will like­ly find this exact set­up for adding a BottomNavigationItem to your NavigationBar.

BottomNavigationItem(
  icon = { Icon(Icons.Filled.Favorite, contentDescription = null) },
  label = { Text(stringResource(screen.resourceId)) },
  selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true,
  onClick = {
    navController.navigate(screen.route) {
      // Pop up to the start destination of the graph to
      // avoid building up a large stack of destinations
      // on the back stack as users select items
      popUpTo(navController.graph.findStartDestination().id) {
        saveState = true
      }

      // Avoid multiple copies of the same destination when
      // reselecting the same item
      launchSingleTop = true

      // Restore state when reselecting a previously selected item
      restoreState = true
    }
  }
)

Also, in just about every piece of doc­u­men­ta­tion you will see calls like the one below as the imple­men­ta­tion for tra­vers­ing your graph.

navController.navigate("home/details")

For the most part this all seems to work great. How­ev­er, when nav­i­gat­ing to a route asso­ci­at­ed with a dif­fer­ent NavigationBarItem it all starts to unravel.

Notably, the fol­low­ing things will happen:

  1. You will notice that the NavigationBarItem has suc­cess­ful­ly tran­si­tioned to the NavigationBarItem as expect­ed with the high­light­ing cor­rect­ly in place. This is great!
  2. Attempt­ing to nav­i­gate back to the ori­gin NavigationBarItem will appear to do noth­ing. Uh-oh!
  3. Pressing/​Gesturing back will pop up in the cur­rent NavigationBarItem route. Again, this seems good?
  4. Now, tap­ping on the ori­gin NavigationBarItem will now be usable. Wait, what!?

This last step is the part that was con­fus­ing me the most. If I removed the screen from the stack, all of my nav­i­ga­tion resumed work­ing again. After a while, I thought maybe it was a bug in how state was being restored. I attempt­ed a workaround — what if my tab just always reset when it was switched to. I mod­i­fied my log­ic to set restoreState = false. It felt wrong, but it worked as I expect­ed. But know­ing that this was not a long term solu­tion, I cre­at­ed the pre­vi­ous­ly men­tioned sam­ple project, bug tick­et, and hoped for the best. For­tu­nate­ly I got a reply quick­ly, but unfor­tu­nate­ly (or for­tu­nate­ly depend­ing on your out­look) they report­ed that it’s not a bug at all.

If you want to emu­late swap­ping to anoth­er tab, make sure to use the exact same flags as your BottomNav uses to actu­al­ly swap from the back stack asso­ci­at­ed with one tab to the back stack asso­ci­at­ed with the oth­er tab.

So how did I fix it? #

It was quick­ly obvi­ous that when I was switch­ing tabs via the BottomNav, I was doing a bunch of extra work that wasn’t hap­pen­ing when I was attempt­ing to nav­i­gate via a but­ton press. Specif­i­cal­ly the fol­low­ing flags:

// Pop up to the start destination of the graph to
// avoid building up a large stack of destinations
// on the back stack as users select items
popUpTo(navController.graph.findStartDestination().id) {
  saveState = true
}

// Avoid multiple copies of the same destination when
// reselecting the same item
launchSingleTop = true

// Restore state when reselecting a previously selected item
restoreState = true

It became clear, that all of my argu­ments need­ed to be shared as this was like­ly going to be a com­mon occur­rence not only in this app, but prob­a­bly any oth­er app I write using nav­i­ga­tion-com­pose, so I cre­at­ed an exten­sion on the NavHostController. Any­time I intend for my appli­ca­tion to switch tabs (and with­in my BottomNavigationItem.onClick) instead of just call­ing nav­i­gate I use the exten­sion switchTabs(route).

fun NavHostController.switchTabs(route: String) {
  navigate(route) {
    // Pop up to the start destination of the graph to
    // avoid building up a large stack of destinations
    // on the back stack as users select items
    popUpTo(graph.findStartDestination().id) {
      saveState = true
    }
    // Avoid multiple copies of the same destination when
    // reselecting the same item
    launchSingleTop = true
    // Restore state when reselecting a previously selected item
    restoreState = true
  }
}
NavigationBarItem(
  ...
  onClick = { navController.switchTabs(tree.route) }
)
 Button(onClick = { navController.switchTabs(Route.HOME_DETAILS) }) {
  ...
}

And, behold! Func­tion­ing navigation! 

View the sam­ple project on GitHub.

Scott Schmitz
Scott Schmitz
Staff Engineer

Looking for more like this?

Sign up for our monthly newsletter to receive helpful articles, case studies, and stories from our team.

How to approach legacy API development
Development

How to approach legacy API development

April 3, 2024

Legacy APIs are complex, often incompatible, and challenging to maintain. MichiganLabs’ digital product consultants and developers share lessons learned for approaching legacy API development.

Read more
Lessons Learned from our Associate Developer
Team

Lessons Learned from our Associate Developer

September 13, 2023

One of our Associate Software Developers, Rohit, reflects on his time at MichiganLabs working on a short-term project, what he learned about real-world development, and the software consultancy business model.

Read more
Advanced Tailwind: Container Queries
Development Web

Advanced Tailwind: Container Queries

July 28, 2023

Explore some advanced web layout techniques using Tailwind CSS framework

Read more
View more articles