22

I'm implementing BottomNavigationView for navigation in an Android app. I am using Fragments to set the content for each tab.

I know how to set up one fragment for each tab and then switch fragments when a tab is clicked. But how can I have a separate back stack for each tab? Here is the code to set up one fragment:

Fragment selectedFragment = ItemsFragment.newInstance();
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
transaction.replace(R.id.content, selectedFragment);
transaction.commit();

For an example, Fragment A and B would be under Tab 1 and Fragment C and D under Tab 2. When the app is started Fragment A is shown and Tab 1 is selected. Then Fragment A might be replaced with Fragment B. When Tab 2 is selected Fragment C should be displayed. If Tab 1 is then selected Fragment B should once again be displayed. At this point, it should be possible to use the back button to show Fragment A.

And Here is the code to set up next fragment in the same tab:

Fragment selectedFragment = ItemsFragment.newInstance();
FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.replace(R.id.content, selectedFragment);
ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
ft.addToBackStack(null);
ft.commit();
AskNilesh
  • 67,701
  • 16
  • 123
  • 163
Rita Azar
  • 701
  • 1
  • 5
  • 14
  • 1
    Unfortunatelly, you'll have to implement that sort of sectioned backstack behaviour yourself... At least i think so. – Shark Aug 10 '17 at 10:39
  • Have you done it before? – Rita Azar Aug 10 '17 at 10:43
  • No, my clients never wanted such broken UX yet, but i have an idea. Something between the current backstack and the https://en.wikipedia.org/wiki/Command_pattern Basically, keep a `HashMap` and add onto / remove from the backstack you want. Will require modifications of the `onBackPressed()` and maybe/probably not using `addToBackstack()` though. – Shark Aug 10 '17 at 10:50
  • 1
    Check this one , https://stackoverflow.com/questions/6987334/separate-back-stack-for-each-tab-in-android-using-fragments – Rajan1404930 Aug 10 '17 at 11:58

5 Answers5

32

Finally, I found the solution, it was inspired by a previous answer on StackOverflow: Separate Back Stack for each tab in Android using Fragments
I only have replaced TabHost with BottomNavigationView and here is the code:
Main Activity

public class MainActivity extends AppCompatActivity {

private HashMap<String, Stack<Fragment>> mStacks;
public static final String TAB_HOME  = "tab_home";
public static final String TAB_DASHBOARD  = "tab_dashboard";
public static final String TAB_NOTIFICATIONS  = "tab_notifications";

private String mCurrentTab;

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

    BottomNavigationView navigation = (BottomNavigationView) findViewById(R.id.navigation);
    navigation.setOnNavigationItemSelectedListener(mOnNavigationItemSelectedListener);

    mStacks = new HashMap<String, Stack<Fragment>>();
    mStacks.put(TAB_HOME, new Stack<Fragment>());
    mStacks.put(TAB_DASHBOARD, new Stack<Fragment>());
    mStacks.put(TAB_NOTIFICATIONS, new Stack<Fragment>());

    navigation.setSelectedItemId(R.id.navigation_home);
}

private BottomNavigationView.OnNavigationItemSelectedListener mOnNavigationItemSelectedListener
        = new BottomNavigationView.OnNavigationItemSelectedListener() {

    @Override
    public boolean onNavigationItemSelected(@NonNull MenuItem item) {
        switch (item.getItemId()) {
            case R.id.navigation_home:
                selectedTab(TAB_HOME);
                return true;
            case R.id.navigation_dashboard:
                selectedTab(TAB_DASHBOARD);
                return true;
            case R.id.navigation_notifications:
                selectedTab(TAB_NOTIFICATIONS);
                return true;
        }
        return false;
    }

};

private void gotoFragment(Fragment selectedFragment)
{
    FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
    fragmentTransaction.replace(R.id.content, selectedFragment);
    fragmentTransaction.commit();
}

private void selectedTab(String tabId)
{
    mCurrentTab = tabId;

    if(mStacks.get(tabId).size() == 0){
      /*
       *    First time this tab is selected. So add first fragment of that tab.
       *    Dont need animation, so that argument is false.
       *    We are adding a new fragment which is not present in stack. So add to stack is true.
       */
        if(tabId.equals(TAB_HOME)){
            pushFragments(tabId, new HomeFragment(),true);
        }else if(tabId.equals(TAB_DASHBOARD)){
            pushFragments(tabId, new DashboardFragment(),true);
        }else if(tabId.equals(TAB_NOTIFICATIONS)){
            pushFragments(tabId, new NotificationsFragment(),true);
        }
    }else {
      /*
       *    We are switching tabs, and target tab is already has atleast one fragment.
       *    No need of animation, no need of stack pushing. Just show the target fragment
       */
        pushFragments(tabId, mStacks.get(tabId).lastElement(),false);
    }
}

public void pushFragments(String tag, Fragment fragment, boolean shouldAdd){
    if(shouldAdd)
        mStacks.get(tag).push(fragment);
    FragmentManager manager = getSupportFragmentManager();
    FragmentTransaction ft = manager.beginTransaction();
    ft.replace(R.id.content, fragment);
    ft.commit();
}

public void popFragments(){
  /*
   *    Select the second last fragment in current tab's stack..
   *    which will be shown after the fragment transaction given below
   */
    Fragment fragment = mStacks.get(mCurrentTab).elementAt(mStacks.get(mCurrentTab).size() - 2);

  /*pop current fragment from stack.. */
    mStacks.get(mCurrentTab).pop();

  /* We have the target fragment in hand.. Just show it.. Show a standard navigation animation*/
    FragmentManager manager = getSupportFragmentManager();
    FragmentTransaction ft = manager.beginTransaction();
    ft.replace(R.id.content, fragment);
    ft.commit();
}

@Override
public void onBackPressed() {
    if(mStacks.get(mCurrentTab).size() == 1){
        // We are already showing first fragment of current tab, so when back pressed, we will finish this activity..
        finish();
        return;
    }

    /* Goto previous fragment in navigation stack of this tab */
    popFragments();
}

}

Home fragment example

public class HomeFragment extends Fragment {
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
    View view = inflater.inflate(R.layout.fragment_home, container, false);
    Button gotoNextFragment = (Button) view.findViewById(R.id.gotoHome2);

    gotoNextFragment.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            ((MainActivity)getActivity()).pushFragments(MainActivity.TAB_HOME, new Home2Fragment(),true);
        }
    });
    return view;
}

}

Rita Azar
  • 701
  • 1
  • 5
  • 14
  • 1
    But how do you save the state of HashMap over screen rotation ? – Kartik Watwani Nov 24 '17 at 01:19
  • First Thanks for the answer, but I got an error on the back press as mStacks.get(mCurrentTab) as null, and in the above goTOFragment not used? pls help – Subin Babu Jun 06 '18 at 06:10
  • @Rita Azar Thanks for the solution. But you are replacing the fragments. It completely destroy previous fragment. It means, when we go to the fragment, where we already have been, it is created again. It leads to re-downlading data from the network(in case of HTTP requests). So, do you know how to solve this problem? – neo Jun 29 '18 at 10:24
  • 1
    @Athul try this one - https://medium.com/@smihajlovskih/create-instagram-like-backstack-4711600c5bff – jlively Aug 13 '18 at 21:04
  • https://medium.com/@smihajlovskih/create-instagram-like-backstack-4711600c5bff link isnt working . – Sumukha Aithal K Sep 26 '18 at 08:33
  • This is what I finding exactly!! really thank u – J.Dragon Dec 27 '20 at 06:07
4

This behavior is supported by the new Navigation Architecture Component (https://developer.android.com/topic/libraries/architecture/navigation/).

Essentially, one can use NavHostFragment, which is a fragment that controls its own back stack:

Each NavHostFragment has a NavController that defines valid navigation within the navigation host. This includes the navigation graph as well as navigation state such as current location and back stack that will be saved and restored along with the NavHostFragment itself. https://developer.android.com/reference/androidx/navigation/fragment/NavHostFragment

Here is an example: https://github.com/deisold/navigation


Edit: Turns out Navigation Architecture Component doesn't support seperate back stacks anyway, as pointed out by the commenters. But as @r4jiv007 mentioned, they are working on it and has offered an "official hack" in the meantime: https://github.com/googlesamples/android-architecture-components/tree/master/NavigationAdvancedSample

Mikael
  • 188
  • 1
  • 3
  • 1
    I don't think it does, at least not with the default configuration. The jetpack navigation does not add the navigation fragments to the back stack, if you go from menu 1 to 2 to 3 and press back it will return to 1 (assuming 1 was setted as the entry fragment in the graph), ignoring 2. This behavior does not seem to be what OP wants. – Allan Veloso Jan 31 '19 at 19:08
  • Navigation Architecture Component doesn't support it , there is an issue open for similar thing, google has provided a hacky solution for it using Navigation Architecture Component but it misses few edge cases. https://issuetracker.google.com/issues/80029773 – r4jiv007 Jul 03 '19 at 15:02
3

It is worth noting that the behavior you describe goes against the Google guidelines. https://material.io/guidelines/components/bottom-navigation.html#bottom-navigation-behavior

Navigation through the bottom navigation bar should reset the task state.

In other words, having Fragment A and Fragment B "inside" Tab 1 is fine, but if the user opens Fragment B, clicks Tab 2, and then clicks Tab 1 again, they should see Fragment A.

Ben P.
  • 52,661
  • 6
  • 95
  • 123
  • 4
    Instagram doesn't do that they keep the backstack intact if you go from one tab to another. – Kartik Watwani Nov 22 '17 at 17:08
  • 4
    As well as Google apps don't go this "guidelines", e. g. Youtube app. – konata Mar 25 '18 at 23:43
  • 5
    That page has changed. Now it says: "Tapping the navigation destination of a previously visited section returns the user to where they left off in that section." https://material.io/design/components/bottom-navigation.html#behavior – Marco Romano Aug 21 '18 at 15:56
  • Instagram is React Native app(kind of hybrid app) and not a native android app – mithil1501 Nov 14 '18 at 09:44
0

Suppose you have 5(A, B, C, D, E) BottomNavigationView menu item, then in Activity create 5 FrameLayout(frmlytA, frmlytB, frmlytC, frmlytD, frmlytE) in parallel overlapping manner as the container for each of these menu items. When BottomNavigation Menu item A is pressed then hide all the other FrameLayouts(Visibility = GONE) and just show(Visibility = VISIBLE) the FrameLayout 'frmlytA' which will host the FragmentA and over this container do the further transactions like (FragmentA -> FragmentX -> FragmentY). And then If user clicks BottomNavigation Menu item B then just hide this(frmlytA) container and show 'frmlytB'. Then if user again presses the menu item A then show 'frmlytA' it should retain the earlier state. So, like this you can switch between the container FrameLayouts and can maintain the back stack of each container.

mithil1501
  • 506
  • 9
  • 20
-6

Instead of using replace method use add fragment,

Instead of this method ft.replace(R.id.content, selectedFragment);

Use this ft.add(R.id.content, selectedFragment);

    Fragment selectedFragment = ItemsFragment.newInstance();
    FragmentTransaction ft = getFragmentManager().beginTransaction();
    ft.(R.id.content, selectedFragment);
    ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
    ft.addToBackStack(null);
    ft.commit();