Android Navigation Component : BottomNavigationView's selected tab icon is not updated
Asked Answered
F

3

18

I'm using BottomNavigationView with Navigation Component. When showing fragment is not root fragment, the tab icon is not updated (selected).

Example:
When I switch between Tab Home with Fragment A (which is root fragment) and Tab Star with Fragment B (which is also root fragment) it is working fine.
But when I navigate from Tab Home to another fragment, like fragment A2, and tap on Tab Star and again return to Tab Home, still Tab Star is selected in BottomNavigationView.

Android Navigation Component Bug

It was working fine with version 2.4.0-alpha05, This is happening when I updated it to 2.5.0-alpha01.

build.gradle (app)

implementation "androidx.navigation:navigation-fragment-ktx:2.5.0-alpha01"
implementation "androidx.navigation:navigation-ui-ktx:2.5.0-alpha01"
implementation "androidx.navigation:navigation-dynamic-features-fragment:2.5.0-alpha01"

build.gradle (root)

classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.5.0-alpha01"

Graph:
Android Navigation Graph

<navigation 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:id="@+id/graph"
        app:startDestination="@id/fragmentA">
    <fragment
            android:id="@+id/fragmentA"
            android:name="ui.test.FragmentA"
            tools:layout="@layout/fragment_test"
            android:label="FragmentA" >
        <action
                android:id="@+id/action_fragmentA_to_fragmentA2"
                app:destination="@id/fragmentA2" />
    </fragment>
    <fragment
            android:id="@+id/fragmentA2"
            android:name="ui.test.FragmentA2"
            tools:layout="@layout/fragment_test"
            android:label="FragmentA2" />
    <fragment
            android:id="@+id/fragmentB"
            android:name="ui.test.FragmentB"
            tools:layout="@layout/fragment_test"
            android:label="FragmentB" />
</navigation>

Menu:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
            android:id="@+id/fragmentA"
            android:icon="@drawable/ic_home"
            android:title="" />
    <item
            android:id="@+id/fragmentB"
            android:icon="@drawable/ic_star"
            android:title="" />
</menu>

Am I doing something wrong? or this is bug?
How can I resolve this problem?

Fcc answered 12/2, 2022 at 4:46 Comment(3)
Please include your navigation graphWilliford
@Williford done.Fcc
I had the same problem and you describe the issue very well.Sweptback
A
24

So what worked for me was the solution that ianhanniballake hinted at in his answer: using setOnItemSelectedListener.

// always show selected Bottom Navigation item as selected (return true)
bottomNavigationView.setOnItemSelectedListener { item ->
  // In order to get the expected behavior, you have to call default Navigation method manually
  NavigationUI.onNavDestinationSelected(item, navController)

  return@setOnItemSelectedListener true
}

This will always select the item and navigate to the associated destination while maintaining multiple back stacks.

Acetaldehyde answered 23/3, 2022 at 9:36 Comment(5)
Yes! Thank you! Was really getting annoyed with this issue and did not want to separate out into multiple nav graphs. Its already difficult enough having multiple activities that share some fragments and having to duplicate nav routes in each graph. Separating even more based on bottom tab would have been horrendous.Pegram
Worked for me too, thank you. I hope it will not break something else :)Holyhead
Yah save me! Thanks for this quick solution!Inflection
works but adds an default animation, is there any way to get rid of the animation or use your own?Fritts
@ueen: Perhaps this might help: https://mcmap.net/q/670264/-how-can-i-change-the-animation-between-tab-switches-with-a-bottomnavigationviewAcetaldehyde
W
8

Given your navigation graph, there is no way to associate fragmentA2 with your menu item fragmentA, so fragmentA is not selected when you return to fragmentA2. As per this issue:

NavigationUI has always used the current destination and what graph it is part of as the source of truth for what tab should be selected.

This can be seen by calling navigate() to go to your SecondFragment - even though you haven't used the bottom nav button, the selected tab was changed because the current destination has changed to R.id.frag_second.

So when you navigate() to R.id.frag_hint via your button in HomeFragment, NavigationUI receives a callback that the current destination has changed to R.id.frag_hint. It looks at that NavDestination and notes that there's no menu item that matches R.id.frag_hint. It then looks at the destination's parent graph - your R.id.sample <navigation> element. There's no menu item that matches that ID either, so NavigationUI can't associated that destination with any menu item and therefore simply does nothing. That is true on all versions of Navigation.

So what is different when you tap on a bottom navigation item? Well, nothing different from a NavigationUI perspective in fact: the exact same code runs and the current destination and what graph it is part of is the source of truth for what tab should be selected. In the Navigation 2.3.5, there was no state saved for each tab, so it only 'worked' because selecting a tab forced the ID of the current destination to match the destination of the menu item you just tapped.

So what you're seeing in your sample app is that there's no link between R.id.frag_hint and any menu item, which means NavigationUI does nothing. If you want to link R.id.frag_hint to your Home tab, then that's exactly what a nested navigation graph can be used for.

I.e., your navigation graph should instead look like:

<navigation
    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:id="@+id/sample"
    app:startDestination="@id/home">
   
    <navigation
        android:id="@+id/home"
        app:startDestination="@id/frag_home">
        <fragment
            android:id="@+id/frag_home"
            android:name="eu.rekisoft.android.navbug.HomeFragment"
            tools:layout="@layout/fragment_home">
            <action
                android:id="@+id/cause_bug"
                app:destination="@id/frag_hint"/>
        </fragment>

        <fragment
            android:id="@+id/frag_hint"
            android:name="eu.rekisoft.android.navbug.HintFragment"
            android:label="Hint"
            tools:layout="@layout/fragment_hint"/>
    </navigation>

    <fragment
        android:id="@+id/frag_second"
        android:name="eu.rekisoft.android.navbug.SecondFragment"
        android:label="Second Fragment"
        tools:layout="@layout/fragment_second"/>

</navigation>

And your menu XML should be updated to use android:id="@id/home" to match your navigation graph.

Now, when you select the Home bottom nav item, the current destination changes to R.id.frag_hint (as your state was restored due to Navigation 2.4's support for multiple back stacks) and NavigationUI looks at the ID - R.id.frag_hint still doesn't match any menu item, but now the parent graph's ID, R.id.home does match a menu item - your Home menu item, hence, it becomes selected.

The intention that your navigation graph and its structure drives the UI is a key part of how NavigationUI works and is working as intended (there was a bug on earlier versions of Navigation 2.4 that broke this driving principle, but that has since been fixed in beta02). All of NavigationUI is built on public APIs specifically so that if you want to use some different logic for which bottom nav item is selected that is independent from your navigation graph structure, you can absolutely do that.

You'll note from the source code that you can call the public onNavDestinationSelected with your MenuItem to get the exact same navigate() logic which retaining your own ability to return any value from the setOnItemSelectedListener (which is what controls whether the tab becomes selected). Similarly, your own OnDestinationChangedListener can choose to look at the hierarchy of a destination to choose whether to change the selected bottom nav item or not.

So your graphs should also be using nested graphs for each tab:

<navigation 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:id="@+id/graph"
        app:startDestination="@id/graphA">
    <navigation
        android:id="@+id/graphA"
        app:startDestination="@id/fragmentA">
        <fragment
            android:id="@+id/fragmentA"
            android:name="ui.test.FragmentA"
            tools:layout="@layout/fragment_test"
            android:label="FragmentA" >
            <action
                android:id="@+id/action_fragmentA_to_fragmentA2"
                app:destination="@id/fragmentA2" />
        </fragment>
        <fragment
            android:id="@+id/fragmentA2"
            android:name="ui.test.FragmentA2"
            tools:layout="@layout/fragment_test"
            android:label="FragmentA2" />
    </navigation>
    <navigation
        android:id="@+id/graphB"
        app:startDestination="@id/fragmentB">
        <fragment
            android:id="@+id/fragmentB"
            android:name="ui.test.FragmentB"
            tools:layout="@layout/fragment_test"
            android:label="FragmentB" />
    </navigation>
</navigation>

Menu:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
            android:id="@+id/graphA"
            android:icon="@drawable/ic_home"
            android:title="" />
    <item
            android:id="@+id/graphB"
            android:icon="@drawable/ic_star"
            android:title="" />
</menu>
Williford answered 12/2, 2022 at 6:0 Comment(5)
I haven't tested it but it looks like to solve this problem, but at what cost? Using the presented structure in a complex project that a fragment is accessible from multiple fragments, forces us to duplicate a lot of code, making the graph very complex and unmaintainable. Also we need to declare/edit a fragment/arguments multiple times every time. That's why I do not prefer using this solution. Also note that it works well in version 2.4.0-alpha05 of the navigation. Also the mentioned issue already has multiple navigations, which makes the case different from mine.Fcc
According to the issue you mentioned, there IS a way for bottom navigation to find out which tab is selected. As it says "It then looks at the destination's parent graph" and in my graph, if it looks at the parent, it can find the fragmentA which matches the navigation menu.Fcc
In your graph, there is no structural relationship that implies that fragmentA2 is at all tied to fragmentA, no moreso than fragmentB or any other random fragment in your graph - you need that shared parent graph that matches your menu item ID to provide that structure that NavigationUI depends on.Williford
So this means that any globally used fragment is out of the question? Similarly, when the same destination (like a detail view) is used from two different menu items, this won't work as the user expects it either, right? So I would have to duplicate that detail view for both menu items? Shouldn't this better be done by inspecting the runtime stack and not the nav graph?Acetaldehyde
works perfectly well. I prefer this solution as the accepted solutions feels like a workaround which could break code in future updates.Systemize
Q
0

I had this issue with the version 2.6.0, if this is your case use 2.5.3, it is works like a charm.

Quirk answered 14/7, 2023 at 2:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.