How to implement proper back navigation for the Android BottomNavigation

Viewed 438

I spent my whole day now looking for an answer and didn't found any solution that suited this question.

I am looking for a way to create a BottomNavigation usage similar to the one of the Instagram or PlayKiosk app. Fragments should be added to the back stack only once. When pressing the back button I'd expect the app to jump back to the last Fragment that was visited and the button of the BottomNavigation to fit that Fragment.

Currently I use the following code:

//BottomNavigationListener
private BottomNavigationView.OnNavigationItemSelectedListener buttonNavigationItemSelectedListener
        = new BottomNavigationView.OnNavigationItemSelectedListener() {

    @Override
    public boolean onNavigationItemSelected(@NonNull MenuItem item) {
        FragmentManager fragmentManager = getSupportFragmentManager();
        switch (item.getItemId()) {
            case R.id.navigation_game:
                currentFragment = GameFragment.newInstance();
                fragmentManager.beginTransaction()
                        .addToBackStack(TAG_FRAGMENT_GAME)
                        .replace(R.id.content_frame, currentFragment, TAG_FRAGMENT_GAME)
                        .commit();
                break;
            case R.id.navigation_tournament:
                currentFragment = TournamentFragment.newInstance();
                fragmentManager.beginTransaction()
                        .addToBackStack(TAG_FRAGMENT_TOURNAMENT)
                        .replace(R.id.content_frame, currentFragment, TAG_FRAGMENT_TOURNAMENT)
                        .commit();
                break;
            case R.id.navigation_history:
                break;
        }
        return true;
    }

};

But this leads to the problem that I could press the button of my BottomNavigation a couple of times and for each of this clicks a new Fragment would be instantiated. Also the BottomNavigation buttons are not set according to the Fragments.

I found this answer but it didn't work out Prevent The Same Fragment From Stacking More Than Once ( addToBackStack)

1 Answers

You can use following structure to have the same working logic of Instagram.

First create a custom limited unique queue class. It only holds last N items, where N is the limit number (or maximum item count) passed to its constructor. Moreover, this type of queue keeps only one instance of a class. If an instance of class A is already in the queue and another instance of class A is to be added to the queue, the former instance is removed first and then the new object is inserted.

public class LimitedUniqueQueue<E> extends LinkedList<E> {

    private int limit;

    public LimitedUniqueQueue(int limit) {
        this.limit = limit;
    }

    @Override
    public boolean add(E o) {
        // For uniqueness
        for (int i = 0; i < super.size(); i++) {
            E item = super.get(i);
            if (item.getClass() == o.getClass()) {
                super.remove(i);
                break;
            }
        }
        boolean added = super.add(o);
        // For size limit
        while (added && size() > limit) {
            super.remove();
        }
        return added;
    }
}

Then update your Activity as follows:

public class MainActivity extends AppCompatActivity {

    private BottomNavigationView navigation;
    private LimitedUniqueQueue<Fragment> queue;
    ...
    private BottomNavigationView.OnNavigationItemSelectedListener onNavigationItemSelectedListener
                = new BottomNavigationView.OnNavigationItemSelectedListener() {

        Fragment gameFragment;
        Fragment tournamentFragment;
        Fragment profileFragment;

        @Override
        public boolean onNavigationItemSelected(@NonNull MenuItem item) {
            FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
            switch (item.getItemId()) {
                case R.id.navigation_game:
                    if (gameFragment == null) {
                        gameFragment = new GameFragment();
                    }
                    transaction.replace(R.id.content, gameFragment).commit();
                    queue.add(gameFragment);
                    return true;
                case R.id.navigation_tournament:
                    if (tournamentFragment == null) {
                        tournamentFragment = new TournamentFragment();
                    }
                    transaction.replace(R.id.content, tournamentFragment).commit();
                    queue.add(tournamentFragment);
                    return true;
                case R.id.navigation_profile:
                    if (profileFragment == null) {
                        profileFragment = new ProfileFragment();
                    }
                    transaction.replace(R.id.content, profileFragment).commit();
                    queue.add(profileFragment);
                    return true;
            }
            return false;
        }

    };

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

        navigation = (BottomNavigationView) findViewById(R.id.navigation);
        queue = new LimitedUniqueQueue<>(navigation.getMenu().size());

        navigation.setOnNavigationItemSelectedListener(onNavigationItemSelectedListener);
        navigation.setSelectedItemId(R.id.navigation_game);
        ...
    }

    @Override
    public void onBackPressed() {
        if (queue.size() > 1) {
            queue.removeLast();
            Fragment previousFragment = queue.getLast();
            if (previousFragment instanceof GameFragment) {
                navigation.setSelectedItemId(R.id.navigation_game);
            } else if (previousFragment instanceof TournamentFragment) {
                navigation.setSelectedItemId(R.id.navigation_tournament);
            } else if (previousFragment instanceof ProfileFragment) {
                navigation.setSelectedItemId(R.id.navigation_profile);
            }
        } else {
            super.onBackPressed();
        }
    }

    ...
}
Related