Search code examples
androidandroid-architecture-navigationandroid-navigation-graph

Navigation graph with multiple top level destinations and back-key behaviors


When I have multiple top level destinations in my navigation graph, and when I click back-key to finish my app, those destinations other than the startDestination won't finish but pop the startDestination Fragment.

  • FragmentA (startDestination) -> finish immediately with back-key
  • FragmentB -> back to FragmentA with back-key
  • FragmentC -> back to FragmentA with back-key

I've tried app:popUpTo="@id/nav_graph" and/or app:popUpToInclusive="true" but in vain.

Activity:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main_activity);

        NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment);

        DrawerLayout drawerLayout = findViewById(R.id.drawer_layout);

        AppBarConfiguration appBarConfiguration =
                new AppBarConfiguration.Builder(R.id.fragmentA, R.id.fragmentB, R.id.fragmentC)
                        .setOpenableLayout(drawerLayout)
                        .build();

        NavigationView navView = findViewById(R.id.nav_view);
        NavigationUI.setupWithNavController(navView, navController);

        Toolbar toolbar = findViewById(R.id.toolbar);
        NavigationUI.setupWithNavController(
                toolbar,
                navController,
                appBarConfiguration
        );
    }
}

layout.xml:

<?xml version="1.0" encoding="utf-8"?>
<!-- Use DrawerLayout as root container for activity -->
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true">

    <!-- Layout to contain contents of navigation_view body of screen (drawer will slide over this) -->
    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <androidx.appcompat.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:background="?attr/colorPrimary"
            android:minHeight="?attr/actionBarSize"
            android:theme="@style/AppTheme.AppBarOverlay"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:popupTheme="@style/AppTheme.PopupOverlay" />

        <fragment
            android:id="@+id/nav_host_fragment"
            android:name="androidx.navigation.fragment.NavHostFragment"
            android:layout_width="0dp"
            android:layout_height="0dp"
            app:defaultNavHost="true"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/toolbar"
            app:navGraph="@navigation/nav_graph" />
    </androidx.constraintlayout.widget.ConstraintLayout>

    <!-- Container for contents of drawer - use NavigationView to make configuration easier -->
    <com.google.android.material.navigation.NavigationView
        android:id="@+id/nav_view"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_gravity="start"
        android:fitsSystemWindows="true"
        app:headerLayout="@layout/navigation_header"
        app:menu="@menu/navigation_view" />

</androidx.drawerlayout.widget.DrawerLayout>

nav_graph:

<?xml version="1.0" encoding="utf-8"?>
<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/fragmentA">

    <fragment
        android:id="@+id/fragmentA"
        android:name="com.stackoverflow.FragmentA"
        android:label="@string/fragmentA">
        <argument
            android:name="message"
            android:defaultValue="(no message)"
            app:argType="string" />
    </fragment>

    <action
        android:id="@+id/toFragmentAWithMessage1"
        app:destination="@id/fragmentA">
        <argument
            android:name="message"
            android:defaultValue="message 1"
            app:argType="string" />
    </action>

    <action
        android:id="@+id/toFragmentAWithMessage2"
        app:destination="@id/fragmentA">
        <argument
            android:name="message"
            android:defaultValue="message 2"
            app:argType="string" />
    </action>

    <fragment
        android:id="@+id/fragmentB"
        android:name="com.stackoverflow.navigationui.FragmentB"
        android:label="@string/fragmentB">
        <argument
            android:name="message"
            android:defaultValue="(no message)"
            app:argType="string" />
    </fragment>

    <action
        android:id="@+id/toFragmentBWithMessage1"
        app:destination="@id/fragmentB">
        <argument
            android:name="message"
            android:defaultValue="message 1"
            app:argType="string" />
    </action>

    <action
        android:id="@+id/toFragmentBWithMessage2"
        app:destination="@id/fragmentB">
        <argument
            android:name="message"
            android:defaultValue="message 2"
            app:argType="string" />
    </action>

    <fragment
        android:id="@+id/fragmentC"
        android:name="com.stackoverflow.FragmentC"
        android:label="@string/fragmentC">
        <argument
            android:name="message"
            android:defaultValue="(no message)"
            app:argType="string" />
    </fragment>

    <action
        android:id="@+id/toFragmentCWithMessage1"
        app:destination="@id/fragmentC">
        <argument
            android:name="message"
            android:defaultValue="message 1"
            app:argType="string" />
    </action>

    <action
        android:id="@+id/toFragmentCWithMessage2"
        app:destination="@id/fragmentC">
        <argument
            android:name="message"
            android:defaultValue="message 2"
            app:argType="string" />
    </action>

</navigation>

I know overriding OnBackPressedCallback is a workaround.

public class FragmentB extends Fragment {

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        requireActivity().getOnBackPressedDispatcher().addCallback(new OnBackPressedCallback(true) {
            @Override
            public void handleOnBackPressed() {
                remove();
                requireActivity().finish();
            }
        });
    }
}

Is there a more fundamental solution?


Solution

  • Just remove or set false to the app:defaultNavHost attribute of the NavHostFragment's <fragment> in the layout.xml:

    <fragment
        android:id="@+id/nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        app:defaultNavHost="false"
        />
    

    Interact programmatically with the Navigation component

    Note that setPrimaryNavigationFragment(finalHost) lets your NavHost intercept system Back button presses. You can also implement this behavior in your NavHost XML by adding app:defaultNavHost="true".

    setPrimaryNavigationFragment

    Set a currently active fragment in this FragmentManager as the primary navigation fragment.

    The primary navigation fragment's child FragmentManager will be called first to process delegated navigation actions such as FragmentManager.popBackStack() if no ID or transaction name is provided to pop to. Navigation operations outside of the fragment system may choose to delegate those actions to the primary navigation fragment as returned by FragmentManager.getPrimaryNavigationFragment().

    Because of your setting true to app:defaultNavHost, your NavHost intercepts back-key presses and leads to pop the startDestination FragmentA.