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.

The benefits of open source technology for businesses
Business Development

The benefits of open source technology for businesses

March 29, 2024

Read more
Web app vs. mobile app: How to decide which is best for your business
Business Development

Web app vs. mobile app: How to decide which is best for your business

March 26, 2024

When considering whether to develop a web app or a mobile app for your business, there’s honestly no definitive answer. But this article will help you make an informed decision that aligns with your business goals and sets you up for success.

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