15

I'm using Jetpack Navigation version 1.0.0-alpha04 with bottom navigation. It works but the navigation doesn't happen correctly. For example, if I have tab A and tab B and from tab A I go to Page C and from there I go to tab B and come back to tab A again, I will see root fragment in the tab A and not page C which does not what I expect.

I'm looking for a solution to have a different stack for each tab, so the state of each tab is reserved when I come back to it, Also I don't like to keep all this fragment in the memory since it has a bad effect on performance, Before jetpack navigation, I used this library https://github.com/ncapdevi/FragNav, That does exactly what, Now I'm looking for the same thing with jetpack navigation.

Ali Khaki
  • 1,184
  • 1
  • 13
  • 24
Alireza Ahmadi
  • 5,122
  • 7
  • 40
  • 67

3 Answers3

16

EDIT 2: Though still no first class support (as of writing this), Google has now updated their samples with an example of how they think this should be solved for now: https://github.com/googlesamples/android-architecture-components/tree/master/NavigationAdvancedSample


The major reason is you only use one NavHostFragment to hold the whole back stack of the app.

The solution is that each tab should hold its own back stack.

  • In your main layout, wrap each tab fragment with a FrameLayout.
  • Each tab fragment is a NavHostFragment and contains its own navigation graph in order to make each tab fragment having its own back stack.
  • Add a BottomNavigationView.OnNavigationItemSelectedListener to BottomNavigtionView to handle the visibility of each FrameLayout.

This also takes care of your "...I don't like to keep all this fragment in memory...", because a Navigation with NavHostFragment by default uses fragmentTransaction.replace(), i.e. you will always only have as many fragments as you have NavHostFragments. The rest is just in the back stack of your navigation graph.

Edit: Google is working on a native implementation https://issuetracker.google.com/issues/80029773#comment25


More in detail

Let's say you have a BottomNavigationView with 2 menu choices, Dogs and Cats.

<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:id="@+id/dogMenu"
        .../>

    <item android:id="@+id/catMenu"
        .../>
</menu>

Then you need 2 navigation graphs, say dog_navigation_graph.xml and cat_navigation_graph.xml.

The dog_navigation_graph might look like

<navigation
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/dog_navigation_graph"
    app:startDestination="@id/dogMenu">
</navigation>

and the corresponding for cat_navigation_graph.

In your activity_main.xml, add 2 NavHostFragments

<FrameLayout
    android:id="@+id/frame_dog"
    ...>

    <fragment
        android:id="@+id/dog_navigation_host_fragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:name="androidx.navigation.fragment.NavHostFragment"
        app:navGraph="@navigation/dog_navigation_graph"
        app:defaultNavHost="true"/>
</FrameLayout>

and underneath add the corresponding for your cat NavHostFragment. On your cat frame layout, set android:visibility="invisible"

Now, in your MainActivity's onCreateView you can

bottom_navigation_view.setOnNavigationItemSelectedListener { item ->
    when (item.itemId) {
        R.id.dogMenu -> showHostView(host = 0)
        R.id.catMenu -> showHostView(host = 1)
    }
    return@setOnNavigationItemSelectedListener true
}

All that showHostView() is doing is toggling the visibility of your FrameLayouts that are wrapping the NavHostFragments. So make sure to save them in some way, e.g. in onCreateView

val hostViews = arrayListOf<FrameLayout>()  // Member variable of MainActivity
hostViews.apply {
    add(findViewById(R.id.frame_dog))
    add(findViewById(R.id.frame_cat))
}

Now it's easy to toggle which hostViews should be visible and invisible.

Algar
  • 5,734
  • 3
  • 34
  • 51
  • 2
    It may work but If I do this I have five fragments in the memory at the same time while only one of them is used. What do you suggest to fix this? – Alireza Ahmadi Sep 03 '18 at 09:11
  • Yeah, that's how you get a good user experience :). Either way, if the user navigates around the app, you want to keep each back stack.. And wasn't that the question to begin with? – Algar Sep 03 '18 at 09:18
  • Well, What I'm trying to achieve is to have separate back stack while only having one fragment at the memory, other fragments exists only in the stack. I know its hard but It should be possible, you can check this out for example https://github.com/ncapdevi/FragNav – Alireza Ahmadi Sep 03 '18 at 09:20
  • Oh, true, now I see your point. That's a nuance worth thinking about! Maybe a `ViewPager` is the way to go, but I haven't tried that with Navigation yet. I'll have to take a look at that. – Algar Sep 03 '18 at 09:23
  • @Reza I though about your point when I wasn't feeling as tired as the first time, and it struck me that there never was an issue. I've updated the answer with an explanation (see `fragmentTransaction.replace()`), and added a full code example. – Algar Sep 05 '18 at 07:44
  • @Algar Is there any update related to this approach? – gromyk May 01 '20 at 21:42
2

The issue has been resolved by the Android team in the latest version 2.4.0-alpha01 multiple backstacks along with bottom navigation support is now possible without any workaround.

https://developer.android.com/jetpack/androidx/releases/navigation

0

First, I want to make an edit to @Algar's answer. The frame that you want to hide should have android:visibility="gone" instead of invisible. The reason for that in your main layout you would have something like this:

    <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=".ui.activity.MainActivity">
    
        <include
            android:id="@+id/toolbar"
            layout="@layout/toolbar_base" />
    
        <FrameLayout
            android:id="@+id/frame_home"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="2"
            >
            <fragment
                android:id="@+id/home_navigation_host_fragment"
                android:name="androidx.navigation.fragment.NavHostFragment"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                app:defaultNavHost="true"
                app:navGraph="@navigation/home_nav" />
        </FrameLayout>
        <FrameLayout
            android:id="@+id/frame_find"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="2"
            android:visibility="gone">
    
            <fragment
                android:id="@+id/find_navigation_host_fragment"
                android:name="androidx.navigation.fragment.NavHostFragment"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                app:defaultNavHost="true"
                app:navGraph="@navigation/find_nav" />
        </FrameLayout>
        ...
    
   </LinearLayout>

If you wrap your main in a LinearLayout, setting the frame to invisible still make that frame counts, so the BottomNavigation wont appear.

Second, you should create a NavHostFragment instance (ie: curNavHostFragment) to keep track of which NavHostFragment is being visible when a tab in BottomNavigation is clicked. Note: you may want to restore this curNavHostFragment when the activity is destroyed by configuration's changes. This is an example:

@Override
protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
    super.onRestoreInstanceState(savedInstanceState);
    //if this activity is restored from previous state,
    //we will have the ItemId of botnav the has been selected
    //so that we can set up nav controller accordingly
    switch (bottomNav.getSelectedItemId()) {
        case R.id.home_fragment:
            curNavHostFragment = homeNavHostFragment;
            ...
            break;
        case R.id.find_products_fragment:
            curNavHostFragment = findNavHostFragment;
            ...
            break;
    
    }
    curNavController = curNavHostFragment.getNavController();
Dennis Nguyen
  • 278
  • 2
  • 9