Reputation: 2835
We're migrating to use the nav component in my company, and its going ok so far.
We have a bottom navigation view with 5 tabs, and using the NavigationUI
to set it up.
We have "Home" as the start destination tab for our nav graph.
Using version 2.4.2
of the navigation-*
libraries.
Each tab now has its own backstack, and its state is retained, however, when:
As pressing back would dismiss the current tab's stack, we get back to the "Home" tab with its previous state intact? (FragmentA pushed on top of it).
We go back to the "Home" tab with only the Home fragment, FragmentA is not showing. And the weird part is, when clicking again (reselecting) the Home tab, it now shows the previously saved state (FragmentA on top of Home).
As this is not the best UX ever, what should be done in this case? is any of those behaviours expected?
Thanks in advance!
Upvotes: 1
Views: 1405
Reputation: 4743
Since Navigation version 2.4.0, BottomNavigationView with NavHostFragment supports a separate back stack for each tab. But it doesn't support back stack for primary tabs. For example, if we have 4 main fragments (tabs) A, B, C, and D, the startDestination
is A. D has child fragments D1, D2, and D3. If user navigates like A -> B -> C ->D -> D1 -> D2-> D3, if the user clicks the back button with the official library the navigation will be D3 -> D2-> D1-> D followed by A. That means primary tabs B and C will not be in the back stack.
To support the primary tab back stack, I created a stack with a primary tab navigation reference. On the user's back click, I updated the selected item of BottomNavigationView based on the stack created.
I have created this Github repo to show what I did. I reached this answer with the following medium articles.
Steps to implement
Add the latest navigation library to Gradle and follow Official repo for supporting back stack for child fragments.
Instead of creating single nav_graph, we have to create separate navigation graphs for each bottom bar item and this three graph should add to one main graph as follows
<navigation
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/nav_graph"
app:startDestination="@+id/home">
<include app:graph="@navigation/home"/>
<include app:graph="@navigation/list"/>
<include app:graph="@navigation/form"/>
</navigation>
And link bottom navigation view and nav host fragment with setupWithNavController
Now the app will support back stack for child fragments. For supporting the main back navigation, we need to add more lines.
private var addToBackStack: Boolean = true
private lateinit var fragmentBackStack: Stack<Int>
The fragmentBackStack will help us to save all the visited destinations in the stack & addToBackStack is a checker which will help to determine if we want to add the current destination into the stack or not.
navHostFragment.findNavController().addOnDestinationChangedListener { _, destination, _ ->
val bottomBarId = findBottomBarIdFromFragment(destination.id)
if (!::fragmentBackStack.isInitialized){
fragmentBackStack = Stack()
}
if (needToAddToBackStack && bottomBarId!=null) {
if (!fragmentBackStack.contains(bottomBarId)) {
fragmentBackStack.add(bottomBarId)
} else if (fragmentBackStack.contains(bottomBarId)) {
if (bottomBarId == R.id.home) {
val homeCount =
Collections.frequency(fragmentBackStack, R.id.home)
if (homeCount < 2) {
fragmentBackStack.push(bottomBarId)
} else {
fragmentBackStack.asReversed().remove(bottomBarId)
fragmentBackStack.push(bottomBarId)
}
} else {
fragmentBackStack.remove(bottomBarId)
fragmentBackStack.push(bottomBarId)
}
}
}
needToAddToBackStack = true
}
When navHostFragment
changes the fragment we get a callback to addOnDestinationChangedListener
and we check whether the fragment is already existing in the Stack or not. If not we will add to the top of the Stack, if yes we will swap the position to the Stack's top. As we are now using separate graph for each tab the id in the addOnDestinationChangedListener
and BottomNavigationView will be different, so we use findBottomBarIdFromFragment
to find BottomNavigationView item id from destination fragment.
private fun findBottomBarIdFromFragment(fragmentId:Int?):Int?{
if (fragmentId!=null){
val bottomBarId = when(fragmentId){
R.id.register ->{
R.id.form
}
R.id.leaderboard -> {
R.id.list
}
R.id.titleScreen ->{
R.id.home
}
else -> {
null
}
}
return bottomBarId
} else {
return null
}
}
And when the user clicks back we override the activity's onBackPressed
method(NB:onBackPressed
is deprecated I will update the answer once I find a replacement for super.onBackPressed()
inside override fun onBackPressed()
)
override fun onBackPressed() {
val bottomBarId = if (::navController.isInitialized){
findBottomBarIdFromFragment(navController.currentDestination?.id)
} else {
null
}
if (bottomBarId!=null) {
if (::fragmentBackStack.isInitialized && fragmentBackStack.size > 1) {
if (fragmentBackStack.size == 2 && fragmentBackStack.lastElement() == fragmentBackStack.firstElement()){
finish()
} else {
fragmentBackStack.pop()
val fragmentId = fragmentBackStack.lastElement()
needToAddToBackStack = false
bottomNavigationView.selectedItemId = fragmentId
}
} else {
if (::fragmentBackStack.isInitialized && fragmentBackStack.size == 1) {
finish()
} else {
super.onBackPressed()
}
}
} else super.onBackPressed()
}
When the user clicks back we will pop the last fragment from Stack and set the selected item id in the bottom navigation view.
Upvotes: 0
Reputation: 598
You can check your fragments are also the same as the navigation id.
for navigation popup, you can use
findNavController().popBackStack()
or
<fragment
android:id="@+id/c"
android:name="com.example.myapplication.C"
android:label="fragment_c"
tools:layout="@layout/fragment_c">
<action
android:id="@+id/action_c_to_a"
app:destination="@id/a"
app:popUpTo="@+id/a"
app:popUpToInclusive="true"/>`
Also, make sure you override the onBackPressed()
method from the
host activity code as:
override fun onBackPressed() { finish() super.onBackPressed() }
Upvotes: 0