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
Software Developer

Stay in the loop with our latest content!

Select the topics you’re interested to receive our new relevant content in your inbox. Don’t worry, we won’t spam you.

Simple, Responsive CSS Grid Page Layout
Development Web

Simple, Responsive CSS Grid Page Layout

June 25, 2019

In this new blog post, Jeff Kloosterman shares his approach to creating a CSS Grid Page Layout. It is simple and it is responsive. Enjoy.

Read more
MichiganLabs among INC. MAGAZINE’S BEST WORKPLACES 2020
Business Team Press Release

MichiganLabs among INC. MAGAZINE’S BEST WORKPLACES 2020

April 30, 2020

Michigan Software Labs is one of the highest-scoring businesses, with standout employee engagement.

Read more
6 Tips for Facilitating Effective Roadmapping and Story Mapping Sessions

6 Tips for Facilitating Effective Roadmapping and Story Mapping Sessions

June 28, 2019

Roadmapping and story mapping are effective ways to align teams around a set direction. It can even help facilitate future conversations of prioritization and backlog grooming.

Read more
View more articles