kostik
kostik

Reputation: 693

jetpack navigation -> multiple back stack with bottom navigation not working when back stack is cleared

Starting with version 2.4.0-alpha01, the NavigationUI helpers support multiple back stacks without any code change. If your app uses the setupWithNavController() methods for BottomNavigationView or NavigationView, all you need to do is to update the dependencies and multiple back stack support will be enabled by default.

In my aplication I have "Intro" screen.(TestFragment)

enter image description here

When user clicks on the button, app is navigating to home screen where Bottom navigation is present.

enter image description here

When navigating from "Intro screen" I want to clear back stack.

<fragment
        android:id="@+id/testFragment"
        android:name="com.example.android.navigationadvancedsample.TestFragment"
        android:label="Test" >

        <action
            app:popUpToInclusive="true"
            app:popUpTo="@id/testFragment"
            android:id="@+id/action_test_to_title"
            app:destination="@id/titleScreen" />
    </fragment>

In this situation multiple back stack funcionality is not working. There is just one back stack. However when property popUpToInclusive is set to false multiple back stack are present and everything is working as expected!

bottom_nav.xml

<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/titleScreen"
        android:icon="@drawable/ic_home"
        android:contentDescription="@string/cd_home"
        android:title="@string/title_home" />
    <item
        android:id="@+id/leaderboard"
        android:icon="@drawable/ic_list"
        android:contentDescription="@string/cd_list"
        android:title="@string/title_list" />
    <item
        android:id="@+id/register"
        android:icon="@drawable/ic_feedback"
        android:contentDescription="@string/cd_form"
        android:title="@string/title_register" />
</menu>

activity_main.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.example.android.navigationadvancedsample.MainActivity">

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/nav_host_container"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        app:defaultNavHost="true"
        app:navGraph="@navigation/nav_graph" />

    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/bottom_nav"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:menu="@menu/bottom_nav"/>
</LinearLayout>

MainActivity.kt

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val navHostFragment = supportFragmentManager.findFragmentById(
            R.id.nav_host_container
        ) as NavHostFragment
        navController = navHostFragment.navController

        // Setup the bottom navigation view with navController
        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)
    }

nav_graph.xml

<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/testFragment">

    <fragment
        android:id="@+id/titleScreen"
        android:name="com.example.android.navigationadvancedsample.homescreen.Title"
        android:label="@string/title_home">
        <action
            android:id="@+id/action_title_to_about"
            app:destination="@id/aboutScreen"/>
    </fragment>
    <fragment
        android:id="@+id/aboutScreen"
        android:name="com.example.android.navigationadvancedsample.homescreen.About"
        android:label="@string/title_about" />
    <fragment
        android:id="@+id/leaderboard"
        android:name="com.example.android.navigationadvancedsample.listscreen.Leaderboard"
        android:label="@string/title_list">
        <action
            android:id="@+id/action_leaderboard_to_userProfile"
            app:destination="@id/userProfile"/>
    </fragment>
    <fragment
        android:id="@+id/userProfile"
        android:name="com.example.android.navigationadvancedsample.listscreen.UserProfile"
        android:label="@string/title_detail">
        <deepLink
            android:id="@+id/deepLink"
            app:uri="www.example.com/user/{userName}"
            android:autoVerify="true"/>
        <argument
            android:name="userName"
            app:argType="string"/>
    </fragment>
    <fragment
        android:id="@+id/register"
        android:name="com.example.android.navigationadvancedsample.formscreen.Register"
        android:label="@string/title_register">
        <action
            android:id="@+id/action_register_to_registered"
            app:destination="@id/registered"/>
    </fragment>
    <fragment
        android:id="@+id/registered"
        android:name="com.example.android.navigationadvancedsample.formscreen.Registered"
        android:label="Registered" />


    <fragment
        android:id="@+id/testFragment"
        android:name="com.example.android.navigationadvancedsample.TestFragment"
        android:label="Test" >

        <action
            app:popUpToInclusive="false"
            app:popUpTo="@id/testFragment"
            android:id="@+id/action_test_to_title"
            app:destination="@id/titleScreen" />
    </fragment>

</navigation>

Why multiple back stack are not supported when navigating from another fragment and clearing back stack? Maybe this is a bug in the jetpack navigation library.

Upvotes: 2

Views: 3678

Answers (2)

Scott Cooper
Scott Cooper

Reputation: 3099

The general advice seems to be "Don't pop the startDestination" but no one really explained why.

If you look at this comment in the NavController, you can see that it won't pop anything if it can't find the start destination in the backstack, hence your BottomNavigation Fragments end up stacking instead of popping.

// We were passed a destinationId that doesn't exist on our back stack. Better to ignore the popBackStack than accidentally popping the entire stack

You can get around this by overriding bottomNavigation.setOnItemSelectedListener and implementing the navigation yourself:

            setOnItemSelectedListener { item ->
                val options = NavOptions.Builder().apply {
                    setLaunchSingleTop(true)
                    setRestoreState(true)
                    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)
                    setPopUpTo(
                        R.id.your_home_fragment,
                        inclusive = false,
                        saveState = true
                    )
                }.build()

                navController.navigate(item.itemId, null, options)
                navController.currentDestination?.hierarchy?.any { it.id == item.itemId } == true
            }

Where R.id.your_home_fragment is the id of the first navigation items Fragment.

Upvotes: 0

OneDev
OneDev

Reputation: 617

I recently walkthrough to your code and found that in your nav_graph.xml you defined testFragment as startDestination, this is the start point of your application, so when you are on welcome screen that you have bottomNavigationView, when you click on the back button, it will return to testFragment Screen, if your app is single activity (single page application) you have to implement OnBackPressedCallback on the first fragment of your welcome screen, some sample code:

OnBackPressedCallback callback = new OnBackPressedCallback(true) {
@Override
public void handleOnBackPressed() {
    NavBackStackEntry backStackEntry = 
getNavController().getPreviousBackStackEntry();
    if (backStackEntry != null) {
        NavDestination destination = backStackEntry.getDestination();
        if (destination != null) {
            if (destination.getId() == R.id.homeFragment) {
                requireActivity().finish();
            }
        }
    }
    }
};
requireActivity().getOnBackPressedDispatcher().addCallback(callback);

Upvotes: 0

Related Questions