Separate Back Stack for each tab in BottomNavigationView Android using Fragments

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.


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


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;
}

}