I managed to create a workaround for it based on androidx.navigation.ui.NavigationUI
internals.
Introduce following interface:
/**
* Use this interface to navigate between bottom tabs instead of findNavController().navigate().
*
* Using navController's navigation can lead to incorrect app states, because each tab
* holds its own backstack. If for example you want to open tab2 from tab1
* by clicking button on tab1 fragment, tab2 fragment will be added to tab1 backstack.
* In the same time tab2 will get highlighted, so clicking on tab1 to navigate back to tab1 backstack
* will have no action, since you are already on tab1.
*
*/
interface BottomTabNavigator {
fun openTab(@IdRes tabId: Int, args: Bundle? = null)
}
Provide the following implementation in the place where you setup your navBar (usually MainActivity
):
override fun openTab(@IdRes tabId: Int, args: Bundle?) {
val navController = (supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment).navController // obtain nav controller. This code may differ depending on the place in which you are implementing this interface
val menuItem = binding.bottomNav.menu.findItem(tabId) // find menu item associated with tabId, this can also slightly differ depending on how you access bottomNav
openTab(
menuItem = menuItem,
navController = navController,
args = args,
restoreState = restoreState
)
}
/**
* Implementation is based on [NavigationUI.onNavDestinationSelected].
* It works similarly, but additionally allows for passing args.
*/
private fun openTab(menuItem: MenuItem, navController: NavController, args: Bundle?, restoreState: Boolean): Boolean {
val builder = NavOptions.Builder().setLaunchSingleTop(true).setRestoreState(true) // you can also add restoreState to openTab in BottomTabNavigator if you need to set it from outside
if (menuItem.order and Menu.CATEGORY_SECONDARY == 0) {
builder.setPopUpTo(
destinationId = navController.graph.findStartDestination().id,
inclusive = false,
saveState = true
)
}
return try {
navController.navigate(
resId = menuItem.itemId,
args = args,
navOptions = builder.build()
)
navController.currentDestination?.matchDestination(item.itemId) == true
} catch (e: IllegalArgumentException) {
false
}
}
// matchDestination is internal in NavigationUI.onNavDestinationSelected, we have to port it as well.
fun NavDestination.matchDestination(@IdRes destId: Int): Boolean =
hierarchy.any { it.id == destId }
You can later use it i.e in your Fragments like:
val bottomTabNavigator = requireActivity() as BottomTabNavigator
bottomTabNavigator.openTab(R.id.myTabId, bundleOf("myKey", "myValue")
I really wish we could just use navController.navigate()
for it, but when I tried navigating to destination that is tied to my tab (as mentioned in docs). Navigation from tab1 to tab2 works, but then pressing tab1 to come back to it stays on tab2.
You can even see it in nav sample:
In Title.kt
replace
view.findViewById<Button>(R.id.about_btn).setOnClickListener {
findNavController().navigate(R.id.action_title_to_about)
}
with
view.findViewById<Button>(R.id.about_btn).setOnClickListener {
findNavController().navigate(
resId = R.id.list,
args = null, // note that I don't pass args here, but it is to show case different issue
navOptions = NavOptions.Builder().setPopUpTo(R.id.list, false).build()
)
}
Run the app and:
- App is opened on Home tab
- Click about button
- It will bring you to Leaderboard tab
- Click Home tab
- It will highlight Home tab, but you will stay on Leaderboard fragment
