Separate Back Stack for each tab in BottomNavigationView Android using Fragments
Asked Answered
D

5

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();
Deference answered 10/8, 2017 at 10:34 Comment(4)
Unfortunatelly, you'll have to implement that sort of sectioned backstack behaviour yourself... At least i think so.Breastfeed
Have you done it before?Deference
No, my clients never wanted such broken UX yet, but i have an idea. Something between the current backstack and the en.wikipedia.org/wiki/Command_pattern Basically, keep a HashMap<CurrentTab, ArrayList<CommandItem> and add onto / remove from the backstack you want. Will require modifications of the onBackPressed() and maybe/probably not using addToBackstack() though.Breastfeed
Check this one , #6987834Podgy
D
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;
}

}

Deference answered 11/8, 2017 at 8:18 Comment(6)
But how do you save the state of HashMap over screen rotation ?Solder
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 helpArius
@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?Mental
@Athul try this one - medium.com/@smihajlovskih/…Ar
medium.com/@smihajlovskih/… link isnt working .Co
This is what I finding exactly!! really thank uGoosestep
Z
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

Zealand answered 4/10, 2018 at 10:45 Comment(2)
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.Triboelectricity
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. issuetracker.google.com/issues/80029773Perhaps
G
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.

Giselegisella answered 10/8, 2017 at 12:29 Comment(4)
Instagram doesn't do that they keep the backstack intact if you go from one tab to another.Solder
As well as Google apps don't go this "guidelines", e. g. Youtube app.Acquittance
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." material.io/design/components/bottom-navigation.html#behaviorAlternant
Instagram is React Native app(kind of hybrid app) and not a native android appKulda
K
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.

Kulda answered 16/11, 2018 at 11:55 Comment(0)
S
-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();
Seagraves answered 10/8, 2017 at 10:43 Comment(1)
using add instead of replace lead to poor performance and OOMTelevise

© 2022 - 2024 — McMap. All rights reserved.