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

Looking for more like this?

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

A Study of Human-Centered Design Off Screen - Burning Man Festival
Design

A Study of Human-Centered Design Off Screen - Burning Man Festival

February 13, 2023

Kourtney examines the Burning Man Festival and its fascinating history of human-centered design

Read more
UX Writing Tips
Design Process

UX Writing Tips

February 3, 2023

Kai shares a few tips he's collected on how to write for user interfaces.

Read more
Chicago Roboto 2022 Retrospective
Android Development

Chicago Roboto 2022 Retrospective

August 11, 2022

Scott Schmitz shares some notes of interest from talks at Chicago Roboto 2022, an Android community conference, that took place August 1-2.

Read more
View more articles