Why does navigation not work in the Navigation Drawer Activity template with version 2.4.1?
Asked Answered
B

1

11

(Using Android Studio 2021.1.1)

Creating a new project using the Navigation Drawer Activity:

  1. Created a default android application with the Navigation Drawer Activity template.
  2. Added a Settings Fragment to the project to test the action_settings menu and config menu items.
  3. Overridded onOptionsItemSelected() in MainActivity.java to handle the settings menu like so:
@Override
public boolean onOptionsItemSelected(MenuItem item) {
    Bundle bundle = new Bundle();
        switch (item.getItemId()) {
            case R.id.action_settings:
                Navigation
                    .findNavController(this, R.id.nav_host_fragment_content_main)
                    .navigate(R.id.nav_settings);
                return true;
            default:
                return super.onOptionsItemSelected(item);
        }
    }
}

Testing:

Run the project, the drawer menu works fine and opens fragments as expected. The problem is that when you click on the overflow menu to open the settings fragment it works, but the drawer menu doesn't work anymore when opening the home fragment.

enter image description here
enter image description here

Observation:

After some testing, I found that it's because of the dependency version, downgrading it to 2.3.5 from 2.4.1 resolves the issue.

Is there something wrong with my code or is it because of an API change? How do I handle this without downgrading?

Extra info:

In MainActivity's onCreate() method I added the following:

     mAppBarConfiguration = new AppBarConfiguration.Builder(
                R.id.nav_home, R.id.nav_gallery, R.id.nav_slideshow, R.id.nav_settings)
                .setOpenableLayout(drawer)
                .build();

app module's build.gradle:

plugins {
    id 'com.android.application'
}

android {
    compileSdk 31

    defaultConfig {
        applicationId "com.example.myapplication"
        minSdk 23
        targetSdk 31
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 
            'proguard-rules.pro'
        }
    }

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

    buildFeatures {
        viewBinding true
    }

    buildToolsVersion '32.0.0'
        ndkVersion '23.1.7779620'
    }

    dependencies {
        implementation 'androidx.appcompat:appcompat:1.4.1'
        implementation 'com.google.android.material:material:1.5.0'
        implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
        implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.1'
        implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1'
        implementation 'androidx.navigation:navigation-fragment:2.4.1'
        implementation 'androidx.navigation:navigation-ui:2.4.1'
        implementation 'androidx.legacy:legacy-support-v4:1.0.0'
        
        testImplementation 'junit:junit:4.13.2'
        androidTestImplementation 'androidx.test.ext:junit:1.1.3'
        androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
    }
}
Beckett answered 21/3, 2022 at 22:53 Comment(0)
A
19

tl/dr: You should be following the Tie destinations to menu items documentation and using NavigationUI.onNavDestinationSelected() to get the right behavior:

@Override
public boolean onOptionsItemSelected(MenuItem item) {
    NavController navController = Navigation.findNavController(this,
        R.id.nav_host_fragment_content_main);

    // By calling onNavDestinationSelected(), you always get the right behavior
    return NavigationUI.onNavDestinationSelected(item, navController)
            || super.onOptionsItemSelected(item);
}

Why

Navigation 2.4 uses multiple back stacks associated with each element in your NavigationView as per the Add a navigation drawer guide:

Starting in Navigation 2.4.0-alpha01, the state of each menu item is saved and restored when you use setupWithNavController.

That means that the 'Home' screen has a back stack associated with it that is restored when you tap on that icon, same for Gallery, Slideshow, and Settings. This is how the state is saved for that item.

This means that each tap on the items in your drawer isn't just navigating to that screen, but is swapping in the entire back stack associated with that item - everything that you've navigated to from that first screen.

So when you call Navigation.findNavController(this, R.id.nav_host_fragment_content_main).navigate(R.id.nav_settings);, you aren't doing the same thing as selecting the Settings item in your drawer - you're just adding the Settings screen to the 'Home' screen's back stack. This is why tapping on the Home icon doesn't do anything - you're already on the 'Home' screen's back stack.

What you actually want to do is swap to the entirely separate back stack associated with the nav_settings item. This would separate the nav_settings back stack from the Home back stack, thus ensuring that tapping the Home icon takes you back to the Home screen's back stack.

This is exactly what the NavigationUI.onNavDestinationSelected() API does (as that's exactly what the setupWithNavController API uses), so you can simply use that directly in your onOptionsItemSelected().

However, if you want to manually call navigate() (which, by the way, means you aren't getting the cross fade animation that you get by default when using onNavDestinationSelected), you can add the saving state flags to your navigate call by applying NavOptions programmatically:

@Override
 public boolean onOptionsItemSelected(MenuItem item) {
    Bundle bundle =new Bundle();
    switch (item.getItemId()) {
        case R.id.action_settings:
        {
            // Manually build the NavOptions that manually do
            // what NavigationUI.onNavDestinationSelected does for you
            NavOptions navOptions = new NavOptions.Builder()
                .setPopUpTo(R.id.nav_home, false, true)
                .setRestoreState(true)
                .build();

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

            navController.navigate(R.id.nav_settings, navOptions);
            return true;
        }

        default:
            return super.onOptionsItemSelected(item);
    }
}

Note that the setupWithNavController API relies on the nested graph of the current destination to determine which item is selected - the expectation is that all destinations in the 'Home' tab are part of the 'Home' navigation graph. So because you've swapped to the nav_settings, setupWithNavController assumes you've swapped to that back stack. Since you haven't actually done that, that's why your selected item gets out of sync with which back stack you are on.

Acyl answered 22/3, 2022 at 4:14 Comment(6)
How to deal with it when one is navigating on event? For example, the user presses a button, or swipes an element, and then navigates from First Fragment to Second Fragment. After that user can move to third destination, but when they choose bottom navigation option for First Fragment they get Second Fragment selected. This bug appears only when Second Fragment is not startDestination, and when it is startDestination, everything works fine. Should I add listener with onOptionsItemSelected on bottom navigation menu to choose fragments there directly?Algonkian
I think, I was not careful enough, or the solution didn't work in my case. The problem with bottom navigation was solved, when I changed fragments into activities, each with its own nav graph. So I was still able to navigate anywhere I wish, had no problems with stack, also than it was easy to add drawer to one activity without changing any other.Algonkian
@Algonkian I had the same problem as you and figured out what was wrong. Suppose you have fragment A, B and C and programmatically navigate using the manual solution from A to B and from B to C. It is important that .setPopUpTo() expects the first parameter to be NOT current fragment but the starting fragment in the graph. So the wright code in fragment B is to use setPopUpTo(nav_A, false, true)Adp
@Adp Thank you. That could be the case. I've checked out to the old version of my project and changed the startDestination. With correct setPopUpTo everything worked fine.Algonkian
@Acyl Thank you. how we can achieve same behaviour in navigation headerview click?Langer
@ianhanniballake, I have the exact same problem when opening the app from notification using NavDeepLinkBuilder.createPendingIntent when the notification destination is different than the start destination. In this case, the navigation is done implicitly in navController.handleDeepLink(). Do you have any suggestions?Sankaran

© 2022 - 2024 — McMap. All rights reserved.