Ahmed Ashraf
Ahmed Ashraf

Reputation: 2835

Android navigation component & Bottom nav view - Hard back button goes back to "Home" with non-saved state

Context

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.

Problem

Each tab now has its own backstack, and its state is retained, however, when:

Expected

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).

What happens

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

Answers (2)

Bincy Baby
Bincy Baby

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.

Medium Link

Upvotes: 0

Maruf Alam
Maruf Alam

Reputation: 598

  1. You can check your fragments are also the same as the navigation id.

  2. 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"/>`
  1. Also, make sure you override the onBackPressed() method from the host activity code as:

    override fun onBackPressed() { finish() super.onBackPressed() }

Upvotes: 0

Related Questions