'Android clear backstack after reselecting Bottom Navigation tab
Using the newest Navigation Component with the BottomNavigationView
, the NavController
now saves and restores the states of tabs by default:
As part of this change, the NavigationUI methods of onNavDestinationSelected(), BottomNavigationView.setupWithNavController() and NavigationView.setupWithNavController() now automatically save and restore the state of popped destinations, enabling support for multiple back stacks without any code changes. When using Navigation with Fragments, this is the recommended way to integrate with multiple back stacks.
This is great! Now switching tabs gives you the last viewed stack.
But, if the user reselects a tab, say they've gone Home -> Detail Page A -> Detail Page B
, then they select the Home
tab expecting to go back to the default view, they still see Detail Page B
.
It seems like part of the discussion was to handle the "reselecting a tab" behavior as mentioned in the issue tracker, but I can't figure out the recommended way for implementing this.
All that's included in the NavigationAdvancedSample is:
val bottomNavigationView = findViewById<BottomNavigationView>(R.id.bottom_nav)
bottomNavigationView.setupWithNavController(navController)
// Setup the ActionBar with navController and 3 top level destinations
appBarConfiguration = AppBarConfiguration(
setOf(R.id.titleScreen, R.id.leaderboard, R.id.register)
)
setupActionBarWithNavController(navController, appBarConfiguration)
And this just restores the previous state, as noted in the release notes.
How can we check for tapping a nav bar item a second time and clear the back stack?
Solution 1:[1]
BottomNavigationView
has its own method for handling reselection via setOnItemReselectedListener()
(or, when using an earlier version of the Material Design Library, the now deprecated setOnNavigationItemReselectedListener()
).
bottomNavigationView.setupWithNavController
does not set this listener (as there is no Material specification for exactly what reselecting a tab should do), so you need to set it yourself:
val bottomNavigationView = findViewById<BottomNavigationView>(R.id.bottom_nav)
bottomNavigationView.setupWithNavController(navController)
// Add your own reselected listener
bottomNavigationView.setOnItemReselectedListener { item ->
// Pop everything up to the reselected item
val reselectedDestinationId = item.itemId
navController.popBackStack(reselectedDestinationId, inclusive = false)
}
Solution 2:[2]
The accepted answer got me started in the right direction. However, with the addition of multiple backstacks supported in Android navigation library 2.4.0, this is what ended up working for me:
val currentRootFragment = supportFragmentManager.findFragmentById(R.id.main_fragment)
val navHost = currentRootFragment as? NavHostFragment
val selectedMenuItemNavGraph = navHost?.navController?.graph?.findNode(menuItem.itemId) as? NavGraph?
selectedMenuItemNavGraph?.let { menuGraph ->
navHost?.navController?.popBackStack(menuGraph.startDestinationId, false)
}
Solution 3:[3]
Use your own setupWithNavController2
instead of setupWithNavController
from androidx.navigation.ui.BottomNavigationViewKt
For ex.:
Added check of already selected item before to navigate :
if (navController.popBackStack(item.itemId, false)) {
return true
}
See comments at onNavDestinationSelected
, full code of setupWithNavController2
:
fun BottomNavigationView.setupWithNavController2(navController: NavController) {
val bottomNavigationView = this
bottomNavigationView.setOnItemSelectedListener { item ->
onNavDestinationSelected(item, navController)
}
val weakReference = WeakReference(bottomNavigationView)
navController.addOnDestinationChangedListener(
object : NavController.OnDestinationChangedListener {
override fun onDestinationChanged(
controller: NavController,
destination: NavDestination,
arguments: Bundle?
) {
val view = weakReference.get()
if (view == null) {
navController.removeOnDestinationChangedListener(this)
return
}
val menu = view.menu
var i = 0
val size = menu.size()
while (i < size) {
val item = menu.getItem(i)
if (matchDestination(destination, item.itemId)) {
item.isChecked = true
}
i++
}
}
})
// Add your own reselected listener
bottomNavigationView.setOnItemReselectedListener { item ->
// Pop everything up to the reselected item
val reselectedDestinationId = item.itemId
navController.popBackStack(reselectedDestinationId, false)
}
}
fun onNavDestinationSelected(
item: MenuItem,
navController: NavController
): Boolean {
val builder = NavOptions.Builder()
.setLaunchSingleTop(true)
if (navController.currentDestination?.parent?.findNode(item.itemId) is ActivityNavigator.Destination) {
builder.setEnterAnim(R.anim.nav_default_enter_anim)
.setExitAnim(R.anim.nav_default_exit_anim)
.setPopEnterAnim(R.anim.nav_default_pop_enter_anim)
.setPopExitAnim(R.anim.nav_default_pop_exit_anim)
} else {
builder.setEnterAnim(R.animator.nav_default_enter_anim)
.setExitAnim(R.animator.nav_default_exit_anim)
.setPopEnterAnim(R.animator.nav_default_pop_enter_anim)
.setPopExitAnim(R.animator.nav_default_pop_exit_anim)
}
if (item.order and Menu.CATEGORY_SECONDARY == 0) {
val findStartDestination = findStartDestination(navController.graph)
if (findStartDestination != null) {
builder.setPopUpTo(findStartDestination.id, false)
}
}
val options = builder.build()
//region The code was added to avoid adding already exist item
if (navController.popBackStack(item.itemId, false)) {
return true
}
//endregion
return try {
// TODO provide proper API instead of using Exceptions as Control-Flow.
navController.navigate(item.itemId, null, options)
true
} catch (e: IllegalArgumentException) {
false
}
}
fun findStartDestination(graph: NavGraph): NavDestination? {
var startDestination: NavDestination? = graph
while (startDestination is NavGraph) {
val parent = startDestination
startDestination = parent.findNode(parent.startDestination)
}
return startDestination
}
fun matchDestination(
destination: NavDestination,
@IdRes destId: Int
): Boolean {
var currentDestination: NavDestination? = destination
while (currentDestination?.id != destId && currentDestination?.parent != null) {
currentDestination = currentDestination.parent
}
return currentDestination?.id == destId
}
Solution 4:[4]
Two methods come to the rescue here...
To update the selection after the item selected (item with back stack, with the latest version - 2.4.2, when there is a back stack present in top destination, selecting that item won't select the item first).
NavigationBarView.setOnItemSelectedListener {}
To wait for the second click and perform popping of the back stack.
NavigationBarView.setOnItemReselectedListener {}
Final code is ,
val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment?
val navController = navHostFragment?.navController
mainBinding?.bottomNavigation?.apply {
navController?.let { navController ->
NavigationUI.setupWithNavController(
this,
navController
)
setOnItemSelectedListener { item ->
NavigationUI.onNavDestinationSelected(item, navController)
true
}
setOnItemReselectedListener {
navController.popBackStack(destinationId = it.itemId, inclusive = false)
}
}
}
Hope this will help..
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | ianhanniballake |
Solution 2 | Callie |
Solution 3 | |
Solution 4 | Willey Hute |