Sunday, May 25, 2014

Using the Navigation Drawer with Otto

    Navigation is one of the most important things to consider when planning the architecture of an app. One type of navigation that has seen a good deal of success is the Navigation Drawer, officially released in the support library during the summer of 2013. The drawer is a swipeable section that can be populated with a view or a fragment, allowing for a lot of freedom when designing what sort of navigations you would want to include in your app, be it a standard list, a spinner, image buttons, etc. that can trigger events in your app, such as displaying a new fragment or activity.

Drawer with items for selecting a new fragment
    For this demo, I have put together a drawer with a list view that changes which fragment is displayed in the main activity on click. These navigation clicks fire events through Otto, a third party event bus library from Square, that lets the main activity know what fragment should be swapped in. All of the code for this working example can be found on GitHub.

    To start, you should declare the DrawerLayout widget in your activity layout file as the root element.

<android.support.v4.widget.DrawerLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <FrameLayout
        android:id="@+id/container"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <com.ptrprograms.navigationdrawer.views.DrawerListView
              android:id="@+id/drawer"
              android:layout_width="240dp"
              android:layout_height="match_parent"
              android:layout_gravity="start"
              android:choiceMode="singleChoice"
              android:divider="@android:color/black"
              android:dividerHeight="0dp"
              android:background="@android:color/background_light" />

</android.support.v4.widget.DrawerLayout>

    The first child element within the drawer layout will be the main layout for your activity, while the second element will be the item within the drawer. In this case I am using a custom ListView. The width should be set to a specific value, such as 240dp, so that it only expands to a certain point on the device screen. The gravity and height for the drawer item should also be set to start and match_parent, respectively, as the drawer by convention should be on the left side of the screen. The other xml properties I have included in my DrawerListView element are simply style specific.

    Now that the layout for the drawer activity is set up, we need to programmatically configure the drawer to work in our main activity. The first step is to enable the home button on the action bar with these two commands:

getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setHomeButtonEnabled(true);

    It should be noted that for this demo I am using the ActionBarCompat support classes, but getSupportActionBar() can be changed to getActionBar() for projects supporting Android 3.0+. Next we need to create the ActionBarDrawerToggle and listener for when the drawer opens and closes:

private void initDrawer() {
    mDrawerToggle = new ActionBarDrawerToggle( this, mDrawerLayout,
            R.drawable.ic_navigation_drawer, R.string.drawer_open_title, R.string.drawer_close_title ) {

        @Override
        public void onDrawerClosed(View drawerView) {
            super.onDrawerClosed(drawerView);
            if( getSupportActionBar() == null )
                return;

            getSupportActionBar().setTitle( R.string.drawer_close_title );
            invalidateOptionsMenu();
        }

        @Override
        public void onDrawerOpened(View drawerView) {
            super.onDrawerOpened(drawerView);
            if( getSupportActionBar() == null )
                return;

            getSupportActionBar().setTitle( R.string.drawer_open_title );
            invalidateOptionsMenu();
        }
    };

    mDrawerLayout.setDrawerListener(mDrawerToggle);

}


    The toggle is created with a context, the DrawerLayout object, a drawable for the action bar top left icon (generally a hamburger icon, and can be generated through Android Asset Studio), and a set of action bar titles to display when the drawer is open or closed. ActionBarDrawerToggle also acts as a listener that supports onDrawerClosed and onDrawerOpened. This is where you can set the action bar titles and hide or show any menu items in the action bar.

Application with closed drawer title and extended hamburger icon (dark purple, top left)
    Two more functions must be added to the activity in order to ensure that the drawer stays open or closed on configuration changes, such as rotating the device:

@Override
protected void onPostCreate(Bundle savedInstanceState) {
    super.onPostCreate(savedInstanceState);
    mDrawerToggle.syncState();
}

@Override
public void onConfigurationChanged(Configuration newConfig) {
    super.onConfigurationChanged(newConfig);
    mDrawerToggle.onConfigurationChanged(newConfig);
}

    The final part of our main activity is a method that uses Otto to subscribe to particular events on the event bus. A separate method is then declared with a @Subscribe declaration to listen for a DrawerNavigationItemClickedEvent. The event has a String value called section that is used for selecting what fragment to display next. Once the fragment is selected and displayed, the drawer is programmatically closed.

@Subscribe
public void onDrawerNavigationClickedEvent( DrawerNavigationItemClickedEvent event ) {
    if( !mCurFragmentTitle.equalsIgnoreCase(event.section) ) {
        if (getString(R.string.fragment_image).equalsIgnoreCase(event.section)) {
            getSupportFragmentManager().beginTransaction().replace(R.id.container, ImageFragment.getInstance()).commit();
        } else if (getString(R.string.fragment_text).equalsIgnoreCase(event.section)) {
            getSupportFragmentManager().beginTransaction().replace(R.id.container, TextFragment.getInstance()).commit();
        } else if (getString(R.string.fragment_number_list).equalsIgnoreCase(event.section)) {
            getSupportFragmentManager().beginTransaction().replace(R.id.container, NumberListFragment.getInstance()).commit();
        }
        mCurFragmentTitle = event.section;
    }
    mDrawerLayout.closeDrawers();
}

    Now that the main activity is set up, let's go over Otto and how it's used to control navigation through the drawer. In the main activity's onCreate method we receive an instance of our navigation bus, which is created using a singleton pattern, and register it in onStart. This is the NavigationBus singleton class:

public class NavigationBus extends Bus {
    private static final NavigationBus navigationBus = new NavigationBus();

    public static NavigationBus getInstance() {
        return navigationBus;
    }

    private NavigationBus() {

    }

}

    In the custom ListView for the drawer, an OnItemClickListener is implemented and the title of the clicked item is pasted in an event over the NavigationBus.

@Override
public void onItemClick(AdapterView<?> adapterView, View view, int position, long l) {
    String drawerText = ( (DrawerItem) adapterView.getAdapter().getItem( position ) ).getDrawerText();
    NavigationBus.getInstance().post( new DrawerNavigationItemClickedEvent( drawerText ) );
}

    The DrawerNavigationItemClickedEvent is the same one that is listened for in our main activity, and that event is declared like so:

public class DrawerNavigationItemClickedEvent {

    public String section;

    public DrawerNavigationItemClickedEvent( String section ) {
        this.section = section;
    }

}

    As can be seen here, Otto allows an application to fire events from any component and catch it in any other component that is listening for that specific event. This makes things such as navigation and passing data from dialogs incredibly simple, saving development time and headaches.

    And with that, we now have a working navigation drawer that allows for changing fragments in our application and providing a proper drawer user experience. If there are any questions, please feel free to comment, otherwise I hope this tutorial is helpful for other developers out there.