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.

Quickly Prototyping a Ktor HTTP API
Development Web

Quickly Prototyping a Ktor HTTP API

August 18, 2022

Whether it’s needing a quick REST API for a personal project, or quickly prototyping a mockup for a client, I like to look for web server frameworks that help me get up and running with minimal configuration and are easy to use. I recently converted a personal project’s API from an Express web server to a Ktor web server and it felt like a breeze. I’ll share below an example of what I found and how easy it is to get a Ktor server up and running.

Read more
How to Prepare for our Associate Software Developer Position
Team

How to Prepare for our Associate Software Developer Position

June 30, 2023

Tips for applying to MichiganLab's Associate Software Developer program

Read more
The benefits of open source technology for businesses
Business Development

The benefits of open source technology for businesses

March 29, 2024

Read more
View more articles