Linking the Navigation Drawer, Back Button and Action Button Indicator
90% of the time when I begin a new Android project, I want 3 things to work out of the box:
- The Navigation Drawer
- The Toolbar
- And, the Back Button
And I want the interaction between them to just work, I want a simple stack of fragments and I want the UI to clearly reflect the backstack.
Unfortunately the Android Studio templates are just not up to this task and they add a lot of code that is either unnecessary, incomplete or just confusing.
Not that I blame them. Linking all three up is not a straightforward task. There is no silver bullet to solve this problem. But there is a neater way.
After doing this several times at the beginning of my projects, I decided to start to abstract away some of the code into base classes: BaseActivity and BaseFragment so that I could quickly use them in another project with a minimum of fuss.
I’m not going to explain all the code in this article, but you can get the latest from github here.
Lets start with the MainActivity’s layout:
<android.support.v4.widget.DrawerLayout
android:id="@+id/main_drawer"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorPrimary">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<android.support.v7.widget.Toolbar
android:id="@+id/main_toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorPrimaryDark"
android:titleTextColor="#ffffff"/>
<FrameLayout
android:id="@+id/main_content"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
<ListView
android:id="@+id/main_navigation"
android:layout_width="200dp"
android:layout_height="match_parent"
android:layout_gravity="left|start"
android:background="?attr/colorPrimaryDark"
android:fitsSystemWindows="true"/>
</android.support.v4.widget.DrawerLayout>
Pretty straight-forward, but some important notes: the ListView is the left menu, it comes last so that it is always on top; the Toolbar and the FrameLayout are the main components of the layout, the FrameLayout is where we are going to throw all the Fragments we cook up, basically stacking them all on top of one another.
[Note that some prefer the Navigation Drawer to slide out underneath the Toolbar, I’ve included an alternate activity_main_toolbar_on_top.xml layout that demonstrates this]
Let’s connect the layout to our MainActivity.class:
public class MainActivity {
@Bind(R.id.main_navigation)
ListView mDrawerList;
@Bind(R.id.main_drawer)
DrawerLayout mDrawerLayout;
@Bind(R.id.main_toolbar)
Toolbar toolbar;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
}
If you’re not using ButterKnife yet… just get out. Go!
Ok, so now we have a List which represents our drawer (you can also add other things to this view, like ImageViews and buttons for say a user profile pic in the Drawer). We have a reference to the Toolbar and we have a reference to the DrawerLayout, which controls the Drawer itself.
We now need an ActionBarDrawerToggle. Think of this as a nice little helper class that you can use to control the little hamburger icon that you can click to make the drawer come out. We also want it to become a ‘Back icon’ when we add a fragment to the stack so the user can touch it to pop the backstack and give that rich intuitive feeling of navigation. Try to let the FragmentManager take care of the back button as much as you can.. it’s already complicated enough.
Now, back to the little top left icon…
There are several states that the icon can be in. It will basically look like a hamburger
or it will look like a back arrow
If the user opens the navigation drawer, usually you want to change it to a back button, clicking the back button will simply close the drawer.
If another fragment is put on the stack, it should turn also change to a back button.
So there are several events that can cause the toggle button to change:
User clicks the toggle button
Since there are only two states the toggle button can be in, you can do 2 things:
- Go ‘back’
- Open the drawer
User presses the back button
You can do only one thing:
- Go ‘back’
The Backstack is pushed or popped
You only have to update the toggle button accordingly:
- sync the toggle button with the backstack
What does “go back” mean?
This is the complicated part. Going back means going through some simple logic:
Is the drawer open? (Yes) Close it. (No) Does the fragment on top want to handle the back click itself? (Yes) Let it. (No) Let Android handle the back press. Is there more than one fragment left on the Backstack? (Yes) Do nothing. (No) Call finish() on the Activity, it’s time to say bye-byes to your app. (that’s right, no interrupting, no ‘are you sure’, just kill the app — don’t be that guy, you’re not clever, you’re annoying).
But it looks ugly when you write it like that, so lets put it into nice clean code:
@Override
public void onBackPressed() {
if (sendBackPressToDrawer()) {
return;
}
if (sendBackPressToFragmentOnTop()) {
return;
} super.onBackPressed(); if (fragmentManager.getBackStackEntryCount() > 0) {
return;
}
finish();
}
I want to handle the Back button in a Fragment
Handling the back press from a Fragment is a two step process. The basic idea is to capture the back press in the Activity, then send it down to the Fragment to see if it wants it.
So you simply need to implement this interface:
public interface BackButtonSupportFragment {
// return true if your fragment has consumed
// the back press event, false if you didn't
boolean onBackPressed();
}
And in the BaseActivity, where we capture the ‘go back’ events you saw this method called:
private boolean sendBackPressToFragmentOnTop() {
BaseFragment fragmentOnTop = fragmentHandler.getCurrentFragment();
if (fragmentOnTop == null) {
return false;
}
if (!(fragmentOnTop instanceof BackButtonSupportFragment)) {
return false;
}
boolean consumedBackPress = ((BackButtonSupportFragment) fragmentOnTop).onBackPressed();
return consumedBackPress;
}
In this method we simply check that there is a fragment on top, that it implements the interface and then send the back event to the fragment so that it can handle it.
I feel obliged to comment that you should almost always send back false from this event. Preventing the back button from operating as expected (ie popping the backstack, hiding the keyboard) and doing something dirty (eg showing an ‘Are you sure? Yes/No’ dialog) is a terrible UX experience and totally against Android’s Guidelines.
The Plumbing
The rest is just hooking up this logic. Listening for the Users actions, listening to the drawer when it’s opened and closed. Listening to the fragment manager for changes to the backstack. And each time one of these events happen, have a look at the state of each and decide what to do.
One thing that’s worth mentioning, there is some logic that I further abstracted into what I called an AddFragmentHandler (for lack of a better name). Many projects I’ve worked on professionally put this logic into some kind of static Utils class. I personally think that’s a bit lazy, but also recognise the convenience of being able to add fragments to the backstack from both the activity and a fragment. And sticking to the DRY principle means that I can’t put the logic into both the BaseActivity and BaseFragment… so I just abstracted it out into a new class and made it a member of both. Any better ideas on how to do that?
This code for example, it is nice to be able to add a fragment from the Fragment and from the Activity.
public void add(BaseFragment fragment) {
//don't add a fragment of the same type on top of itself.
BaseFragment currentFragment = getCurrentFragment();
if (currentFragment != null) {
if (currentFragment.getClass() == fragment.getClass()) {
Log.w("Fragment Manager", "Tried to add a fragment of the same type to the backstack. This may be done on purpose in some circumstances but generally should be avoided.");
return;
}
}
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
fragmentTransaction.replace(R.id.main_content, fragment, fragment.getTitle());
fragmentTransaction.addToBackStack(fragment.getTitle());
fragmentTransaction.commit();
}
Notice that there is a mechanism to prevent fragments of the same type being added on top of each other. This may not be appropriate in all cases.
Keeping the title in sync
This is a simple matter of setting the title in the BaseFragment’s onResume() method and forcing all subclasses to provide a title.
public abstract class BaseFragment extends Fragment {
@Override
public void onResume() {
super.onResume();
getActivity().setTitle(getTitle());
}
protected abstract String getTitle();
}
Because it’s a task that I’ve had to do so many times, it makes sense to have a template to start your Android project from. Feel free to use mine, or better yet to improve it!