19

I am using a single activity and multiple fragments(screenshot attached) within the same activity to provide a seamless navigation. But after implementing the latest toolbar and navigation view, it seems hard to handle the navigation and home buttons. I am having trouble with the following things.

  • Managing the Hamburger/Back button at left top. Toggling the icon and functionality to Menu and Back nav.
    • Page title - Changing the page titles whenever a fragment in pushed and popped.

I have tried several things like overriding onBackPressed(), setHomeAsUpIndicator, popping fragments manually. Earlier i was using ActionBarDrawer toggle to handle this but it is failing somehow now. I checked the google samples they seem to use separate activities at most of the places.

Can anyone guide me how to implement a proper back navigation to handle the NavigationView, Back button in inner fragments and page titles? I am using AppCompatActivity, android.app.Fragment, NavigationView and Toolbar.

Fragment 1 -> Fragment 2 -> Fragment 3

Ajith M A
  • 3,838
  • 3
  • 32
  • 55
  • "Earlier i was using ActionBarDrawer toggle to handle this which is not available with NavigationView." - Why can't you use an `ActionBarDrawerToggle` with a `NavigationView`, exactly? – Mike M. Apr 12 '16 at 07:46
  • 1
    Take a look here, be sure to read the last comment on the answer regarding the drawer toggle: http://stackoverflow.com/questions/34025331/how-can-i-control-the-activitys-up-button-from-a-contained-fragment – Daniel Nugent Apr 12 '16 at 07:49
  • I have tried this. It's not working as expected. In the inner fragment, home button changes to Back button, but still it acts like the same old menu button. When you press the system back and reach back home screen, the home icon disappears. I ended up with these two issues. I have tried adding the drawer toggle as mentioned in comment. Even that doesn't work. – Ajith M A Apr 12 '16 at 11:17
  • Add the relevant code you have so far to the question – Daniel Nugent Apr 12 '16 at 14:38
  • Try my [answer](http://stackoverflow.com/questions/36566726/implementing-proper-back-navigation-and-home-button-handling-using-toolbar-in-an/36677197#36677197), it handles all use cases you want. – Rohit Arya Apr 19 '16 at 06:24
  • @AjithMemana I updated my answer, I also don't use anymore `ActionBarDrawerToggle` – Yoann Hercouet Apr 19 '16 at 09:09

6 Answers6

16

It's much easier to illustrate with some sort of division of responsibility for your Activity and Fragment.

Division of responsibilities for Activity and Fragment Problem 1: Managing the Hamburger/Back button at left top. Toggling the icon and functionality to Menu and Back nav.

From the illustration, the solution should be encapsulated by the Activity, which will look something like this:

public class MainActivity extends AppCompatActivity implements NavigationView.OnNavigationItemSelectedListener {

    private ActionBarDrawerToggle mDrawerToggle;
    private DrawerLayout mDrawer;
    private ActionBar mActionBar;

    private boolean mToolBarNavigationListenerIsRegistered = false;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
        mActionBar = getSupportActionBar();

        mDrawer = (DrawerLayout) findViewById(R.id.drawer_layout);
        mDrawerToggle = new ActionBarDrawerToggle(this, mDrawer, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close);
        mDrawer.addDrawerListener(mDrawerToggle);
        mDrawerToggle.syncState();

        NavigationView navigationView = (NavigationView) findViewById(R.id.nav_view);
        navigationView.setNavigationItemSelectedListener(this);

        // On orientation change savedInstanceState will not be null.
        // Use this to show hamburger or up icon based on fragment back stack.
        if(savedInstanceState != null){
            resolveUpButtonWithFragmentStack();
        } else {
            // You probably want to add your ListFragment here.
        }
    }

    @Override
    public void onBackPressed() {

        if (mDrawer.isDrawerOpen(GravityCompat.START)) {
            mDrawer.closeDrawer(GravityCompat.START);

        } else {
            int backStackCount = getSupportFragmentManager().getBackStackEntryCount();

            if (backStackCount >= 1) {
                getSupportFragmentManager().popBackStack();
                // Change to hamburger icon if at bottom of stack
                if(backStackCount == 1){
                    showUpButton(false);
                }
            } else {
                super.onBackPressed();
            }
        }
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();

        //noinspection SimplifiableIfStatement
        if (id == R.id.action_settings) {
            return true;

        } else if (id == android.R.id.home) {
            // Home/Up logic handled by onBackPressed implementation
            onBackPressed();
        }

        return super.onOptionsItemSelected(item);
    }

    @SuppressWarnings("StatementWithEmptyBody")
    @Override
    public boolean onNavigationItemSelected(MenuItem item) {
        // Handle navigation view item clicks here.
        int id = item.getItemId();

        // Navigation drawer item selection logic goes here

        mDrawer.closeDrawer(GravityCompat.START);
        return true;
    }

    private void replaceFragment() {
        /**
        * Your fragment replacement logic goes here
        * e.g.
        * FragmentTransaction ft = getFragmentManager().beginTransaction();
        * String tag = "MyFragment";
        * ft.replace(R.id.content, MyFragment.newInstance(tag), tag).addToBackStack(null).commit();
        */

        // The part that changes the hamburger icon to the up icon
        showUpButton(true);
    }

    private void resolveUpButtonWithFragmentStack() {
        showUpButton(getSupportFragmentManager().getBackStackEntryCount() > 0);
    }

    private void showUpButton(boolean show) {
        // To keep states of ActionBar and ActionBarDrawerToggle synchronized,
        // when you enable on one, you disable on the other.
        // And as you may notice, the order for this operation is disable first, then enable - VERY VERY IMPORTANT.
        if(show) {
            // Remove hamburger
            mDrawerToggle.setDrawerIndicatorEnabled(false);
            // Show back button
            mActionBar.setDisplayHomeAsUpEnabled(true);
            // when DrawerToggle is disabled i.e. setDrawerIndicatorEnabled(false), navigation icon
            // clicks are disabled i.e. the UP button will not work.
            // We need to add a listener, as in below, so DrawerToggle will forward
            // click events to this listener.
            if(!mToolBarNavigationListenerIsRegistered) {
                mDrawerToggle.setToolbarNavigationClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        onBackPressed();
                    }
                });

                mToolBarNavigationListenerIsRegistered = true;
            }

        } else {
            // Remove back button
            mActionBar.setDisplayHomeAsUpEnabled(false);
            // Show hamburger
            mDrawerToggle.setDrawerIndicatorEnabled(true);
            // Remove the/any drawer toggle listener 
            mDrawerToggle.setToolbarNavigationClickListener(null);
            mToolBarNavigationListenerIsRegistered = false;
        }

        // So, one may think "Hmm why not simplify to:
        // .....
        // getSupportActionBar().setDisplayHomeAsUpEnabled(enable);
        // mDrawer.setDrawerIndicatorEnabled(!enable);
        // ......
        // To re-iterate, the order in which you enable and disable views IS important #dontSimplify.
    }
}

Problem 2: Page title - Changing the page titles whenever a fragment in pushed and popped.

Essentially, this can be handled in the onStart for each Fragment i.e. your ListFragment, DetailsFragment and CommentsFragment look something like this:

@Override
public void onStart() {
    super.onStart();
    // where mText is the title you want on your toolbar/actionBar
    getActivity().setTitle(mText);
}

Probably worth having setRetainInstance(true) in the onCreate of your fragments as well.

ade.akinyede
  • 2,214
  • 1
  • 13
  • 17
  • Thanks, it nearly works. The only issue now i have is that in the detail fragment(second level), the menu turns to back button, but the back action is not working. OnBackPressed in activity is not invoked? Is there any standard method to handle the back press(<- button in Toolbar) and pop the fragment. – Ajith M A Apr 18 '16 at 14:04
  • I'm also assuming your Activity theme extends `Theme.AppCompat.Light.NoActionBar` – ade.akinyede Apr 18 '16 at 22:11
  • Nice solution! Worked for me. – Ryan Newsom Apr 01 '17 at 23:35
5

tl;dr

Watch this: https://youtu.be/ANpBWIT3vlU

Clone this: https://github.com/shredderskelton/androidtemplate.

This is a really common problem and one that I've overcome by creating a kind of template project which I use whenever I start a new Android project. The idea is to abstract as much of the logic that handles the back button, the 'hamburger' indicator and fragment management into reusable classes:

Start by creating a BaseActivity and BaseFragment class. This is where you are going to as much of the reusable code as possible.

Lets start with your BaseActivity

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    fragmentManager = getSupportFragmentManager();
    fragmentHandler = new AddFragmentHandler(fragmentManager);
    fragmentManager.addOnBackStackChangedListener(backStackListener);
}

The FragmentManager is the key to owning the back stack, so you need to listen for changes to the back stack from here. The AddFramentHandler is a little class I cooked up to make it easier to add Fragments, from Fragments. More on that later.

@Override
public void onBackPressed() {
    if (sendBackPressToDrawer()) {
        //the drawer consumed the backpress
        return;
    }

    if (sendBackPressToFragmentOnTop()) {
        // fragment on top consumed the back press
        return;
    }

    //let the android system handle the back press, usually by popping the fragment
    super.onBackPressed();

    //close the activity if back is pressed on the root fragment
    if (fragmentManager.getBackStackEntryCount() == 0) {
        finish();
    }
}

onBackPressed is where most of the magic happens. You notice the plain text formatting of the methods.. I'm a huge Clean Code fan - if you need to write comments, your code isn't clean. Basically you need to really have a central place where you can run to when you're not sure why a back button press is not happening the way you expect. This method is that place.

private void syncDrawerToggleState() {
    ActionBarDrawerToggle drawerToggle = getDrawerToggle();
    if (getDrawerToggle() == null) {
        return;
    }
    if (fragmentManager.getBackStackEntryCount() > 1) {
        drawerToggle.setDrawerIndicatorEnabled(false);
        drawerToggle.setToolbarNavigationClickListener(navigationBackPressListener); //pop backstack
    } else {
        drawerToggle.setDrawerIndicatorEnabled(true);
        drawerToggle.setToolbarNavigationClickListener(drawerToggle.getToolbarNavigationClickListener()); //open nav menu drawer
    }
}

This is the other key part of the BaseActivity. Basically this method checks whether you are at the root fragment and sets up the indicator accordingly. Notice that it changes the listener depending on how many fragments are in the back stack.

Then there is the BaseFragment:

@Override
public void onResume() {
    super.onResume();
    getActivity().setTitle(getTitle());
}

protected abstract String getTitle();

The code above shows how the title is handled by the fragments.

shredder
  • 1,438
  • 13
  • 12
  • Please update your answer with complete code. sendBackPressToDrawer and sendBackPressToFragmentOnTop are missing – Johny19 Jan 17 '18 at 14:03
1

"Page title - Changing the page titles whenever a fragment in pushed and popped"

When you remove a fragment, there is the method isRemoving(). It helps to change title back.

@Override
public void onStop() {
    super.onStop();
    if (isRemoving()) {
        // Change your title here
    }
}

"functionality to Menu and Back nav"

Suggestion: we have to rely on the default android navigation system. If we use addToBackStack() for our fragments, in theory we don't have to override onBackPressed() at all.

  1. "App does not redefine the expected function of a system icon (such as the Back button)."
  2. "App supports standard system Back button navigation and does not make use of any custom, on-screen "Back button" prompts."

Core App Quality: https://developer.android.com/distribute/essentials/quality/core.html

"Managing the Hamburger/Back button at left top"

I suggest to use activity instead of 'MainActivityDetailFragment' to avoid complication.

Vadim Kotov
  • 8,084
  • 8
  • 48
  • 62
Dmitry
  • 149
  • 1
  • 7
  • The idea of using MainActivityDetailFragment works well. But apart from this is there really a way to handle the issue without actually messing up with the system onBackPressed? When i checked Gmail app, they are handling the messages list and activity in a single activity. – Ajith M A Apr 18 '16 at 15:06
  • How do you add your (nested) fragments via getSupportFragmentMananger() or getFragmentManager()? Implement Back Navigation for Fragments: http://developer.android.com/intl/ru/training/implementing-navigation/temporal.html – Dmitry Apr 19 '16 at 20:12
1

Try something like this:

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

    toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);
    if (getSupportActionBar()!=null) {
        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
    }

    drawer = (DrawerLayout) findViewById(R.id.drawer_layout);

    final ActionBarDrawerToggle drawerToggle = new ActionBarDrawerToggle(
            this, drawer, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close);
    drawer.addDrawerListener(drawerToggle);
    drawerToggle.syncState();

    final View.OnClickListener originalToolbarListener = drawerToggle.getToolbarNavigationClickListener();

    final View.OnClickListener navigationBackPressListener = new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            getFragmentManager().popBackStack();
        }
    };

    getFragmentManager().addOnBackStackChangedListener(new FragmentManager.OnBackStackChangedListener() {
        @Override
        public void onBackStackChanged() {
            if (getFragmentManager().getBackStackEntryCount() > 0) {
                drawerToggle.setDrawerIndicatorEnabled(false);
                drawerToggle.setToolbarNavigationClickListener(navigationBackPressListener);
            } else {
                drawerToggle.setDrawerIndicatorEnabled(true);
                drawerToggle.setToolbarNavigationClickListener(originalToolbarListener);
            }
        }
    });

    // Though below steps are not related but I have included to show drawer close on Navigation Item click. 

    navigationView = (NavigationView) findViewById(R.id.nav_view);
    navigationView.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener() {
        @Override
        public boolean onNavigationItemSelected(MenuItem item) {
            int id = item.getItemId();
            /**
             * handle item clicks using id
             */
            drawer.closeDrawer(GravityCompat.START);
            return true;
        }
    });
}

Handle the drawer state onBackPressed:

@Override
public void onBackPressed() {
    if (drawer.isDrawerOpen(GravityCompat.START)) {
        drawer.closeDrawer(GravityCompat.START);
    } else {
        super.onBackPressed();
    }
}

To reload previous fragment on back press, always add the fragment transaction to back stack like this:

FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
SomeFragment fragmentToBeLoaded = new SomeFragment();
fragmentTransaction.replace(R.id.fragment_container, fragmentToBeLoaded,
                fragmentToBeLoaded.getName());
fragmentTransaction.addToBackStack(fragmentToBeLoaded.getName());
fragmentTransaction.commit();

To dynamically change the page title, you can call this from every Fragments onStart or onResume method:

@Override
public void onStart() {
   super.onStart();
   getActivity().setTitle("Title for fragment");
}

Note: I have considered standard layout declaration and thus I have not included any layouts.

Rohit Arya
  • 6,751
  • 1
  • 26
  • 40
0

Add this in your MainActivity where you are calling Fragments. getBackStackEntryCount() Return number of fragments in the back stack. where the fragment on the bottom of the stack has index 0. popBackStack() Pop the top Fragment off the back stack

 @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        int id = item.getItemId();

        if (id == android.R.id.home) {
            if (getSupportFragmentManager().getBackStackEntryCount() == 1) {
                getSupportFragmentManager().popBackStack();
            } else {
                super.onBackPressed();
            }
        }
        return true;
    }

And in your Fragment where you want to go back use this function

  @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        int id = item.getItemId();
        if (id == android.R.id.home) {
            getActivity().onBackPressed();
        }
        return true;
    }
Umer
  • 1,566
  • 2
  • 20
  • 31
0

Ok, after a lot of tests I finally succeeded to setup a good navigation. I needed exactly the same as you, the only difference is that I am using v4 Fragments, but I don't think this will change anything here.

I am not using ActionBarDrawerToggle since the latest examples from Google do not use this component anymore.

The solution below also works for deep navigation: parent activity --> fragment --> fragment etc.

The only change needed in the Fragments is to change the title:

@Override
public void onActivityCreated(Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);

    getActivity().setTitle(R.string.targets);
}

In the parent Activity onCreate method, I initialize the following:

    mNavigationView = (NavigationView) findViewById(R.id.navigation_view);
    setupDrawerContent(mNavigationView);

    final Toolbar toolbar = (Toolbar) findViewById(R.id.drawer_toolbar);
    setSupportActionBar(toolbar);

    getSupportActionBar().setHomeAsUpIndicator(R.drawable.ic_menu_24);// Set the hamburger icon
    getSupportActionBar().setDisplayHomeAsUpEnabled(true);// Set home button pressable

    // Handle the changes on the actionbar
    getSupportFragmentManager().addOnBackStackChangedListener(
            new FragmentManager.OnBackStackChangedListener() {
                public void onBackStackChanged() {
                    // When no more fragments to remove, we display back the hamburger icon and the original activity title
                    if (getSupportFragmentManager().getBackStackEntryCount() <= 0) {
                        getSupportActionBar().setHomeAsUpIndicator(R.drawable.ic_menu_24);
                        setTitle(R.string.app_name);
                    }
                    // Else displays the back arrow
                    else {
                        getSupportActionBar().setHomeAsUpIndicator(R.drawable.ic_arrow_back_24);
                    }
                }
            });

Here is now the code to handle the action on the Home button:

@Override
public boolean onOptionsItemSelected(MenuItem item){
    // Close the soft keyboard right away
    Tools.setSoftKeyboardVisible(mViewPager, false);

    switch (item.getItemId()) {
        case android.R.id.home:

            // When no more fragments to remove, open the navigation drawer
            if (getSupportFragmentManager().getBackStackEntryCount() <= 0) {
                mDrawerLayout.openDrawer(GravityCompat.START);
            }
            // Removes the latest fragment
            else {
                getSupportFragmentManager().popBackStack();
            }

            return true;
    }
    return super.onOptionsItemSelected(item);
}

And finally the code to handle the back press action:

@Override
public void onBackPressed() {
    // When no more fragments to remove, closes the activity
    if (getSupportFragmentManager().getBackStackEntryCount() <= 0) {
        super.onBackPressed();
    }
    // Else removes the latest fragment
    else {
        getSupportFragmentManager().popBackStack();
    }
}

NOTE: I am using an AppCompatActivity, a NavigationView and the theme Theme.AppCompat.Light.NoActionBar.

Yoann Hercouet
  • 17,894
  • 5
  • 58
  • 85