android

Android Drawer and Fragment Navigation. A more “real life” scenario

Hey, I’m adding more meat into these series of fragment navigation tutorials. If you have been following my past entries you should have some knowledge on the Application Drawer design pattern and fragment navigation. If not, go and take a look at this post.

So far I have shown you how to create a very simple app: a home screen that allows to navigate to other sections via the drawer. Once you have reached one level of navigation the drawer is no longer accesible and you will have to navigate back to the home to reach it again.

Although you can absolutely build an app on that premise, in a real life scenario you may need to keep the drawer in a second level of navigation. I’m going to use the GMAIL app as an example here.

The Gmail app contains a drawer from where you can access different folders. When the app is launched you see the inbox, but if you open the side menu you can reach sent, trash, etc. I want you to pay attention to the following: From the inbox, open the drawer and then navigate to any section, lets say “Starred”. Once you are there (notice you can open the drawer from this fragment), push your back button. You have returned to inbox again. This is pretty much expected, but let’s analyse another scenario:

Launch the Gmail app and navigate to a folder you like (I’ll choose Sent). As before, you are no longer on the inbox fragment but the drawer is still accessible. If we think in terms of fragment transactions we have two: Inbox > Sent. Now, instead of pressing the back key, open the drawer and navigate to a section different to inbox or the one you choose. I’ll pick Drafts. Again, in terms of transactions we have: Inbox > Sent > Drafts.

Now it get’s interesting: press the Back key and see what happens. You will notice that you will return directly to Inbox skipping  the Send transaction. This is an example of good application navigation.

In a real life scenario you will wan’t to copy this kind of behavior: although maybe you didn’t notice you were expecting the behavior your got. People are used to this kind of navigation, trying to reinvent the wheel may not be the best thing to do. Also, whom better to copy than Google!

Before writing some code let’s present some kind of diagram to visualize what we want to achieve:app_view_navigation.png

drawer.gif

 

We are going to build a reduced Gmail clone: the app entry point will be Inbox and from there you will be able to access other sections. This time, the drawer will be present on other sections as well (in this case Inbox, Sent and Trash share the drawer).  Conceptually you have to think about the fragments that share the drawer as root for a section. We will define a base fragment for our app with the properties needed to achieve this behavior first (all the code on this section will be very similar to the previous tutorial):

public abstract class MyAppFragment extends Fragment {

    protected AppSection appSection;

    /**
     * get a unique string identifier for this fragment. Can be used as a key to add
     * into the back stack
     *
     * @return unique tag
     */
    public String getFragmentTag() {
        return this.appSection.toString();
    }

    /**
     * obtain the current instance of the activity holding this fragment
     *
     * @return main activity instance
     */
    public MainActivity getMainActivity() {
        return (MainActivity) getActivity();
    }

    /**
     * this will indicate the navigation that the stack will be cleared before inserting a new fragment transaction
     */
    protected boolean isRootSection = false;

    public boolean getIsRootSection() {
        return this.isRootSection;
    }
}

There are some minor changes since the last time: the fragment tag is related to an Enum used to build fragments and a new variable indicating a fragment can be a section root is added. I won’t be showing the factory code again, you can review this on the previous section. Instead I’ll focus on the MainActivity and how the navigation is handled.

Defining Some Helper Methods

We will refactor some of the past login into methods (we will add new logic as well):

/**
 * Updates the sidebar according to a specific fragment
 * @param fragment fragment being shown
 */
private void setNavigationViewCheckedItem(MyAppFragment fragment){
    if(fragment.getFragmentTag().equals(AppSection.INBOX.toString()))
        navigationView.setCheckedItem(R.id.nav_inbox);
    else if(fragment.getFragmentTag().equals(AppSection.SENT.toString()))
        navigationView.setCheckedItem(R.id.nav_sent);
    else if(fragment.getFragmentTag().equals(AppSection.TRASH.toString()))
        navigationView.setCheckedItem(R.id.nav_trash);
}

/**
 * Get the current visible MyAppFragment.
 *
 * @return current visible MyAppFragment. null if the stack is empty or fragment is not MyAppFragment
 */
private MyAppFragment getCurrentFragment() {
    List<Fragment> fragmentStack = getSupportFragmentManager().getFragments();
    if (fragmentStack == null || fragmentStack.size() == 0)
        return null;

    for (Fragment current : fragmentStack) {
        if (current != null && current.isVisible() && (current instanceof MyAppFragment))
            return (MyAppFragment) current;
    }

    return null;
}

//region application bar

/**
 * Display the hamburger icon on the application bar
 */
private void showRootNavigation() {
    getSupportActionBar().setDisplayHomeAsUpEnabled(false);
    toggle.setDrawerIndicatorEnabled(true);
    toggle.syncState();
    drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
}

/**
 * Display the back arrow on the application bar
 */
private void showBackArrowNavigation() {
    // order of these calls IS important
    toggle.setDrawerIndicatorEnabled(false);
    getSupportActionBar().setDisplayHomeAsUpEnabled(true);
    drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
}
//endregion

Some of those methods are trivial, specially the ones related to changing the look and feel of the application bar. The method that checks an option of the sidebar menu is quite simple as well.

The method that returns the current fragment may need some analysis but it’s not that hard: the idea is to retrieve the current visible fragment (of our type).

Dealing with the navigation, going and returning from a fragment

Now we get to deal with two operations: navigating to a section and returning from it (pressing back for example). Let’s see how we navigate to a new fragment:

/**
 * Navigates to a fragment within the app
 *
 * @param section    where to go
 * @param addToStack should this transaction be added to the back stack ?
 */
public void navigateToSection(AppSection section, boolean addToStack) {

    // obtain the fragment we want to navigate to
    MyAppFragment fragment = MyAppFragmentFactory.getFragment(section);

    // if this is a root fragment, clear everything up to the home and add this one
    if (fragment.getIsRootSection())
        getSupportFragmentManager().popBackStackImmediate(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);

    // normal transaction stuff
    FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
    fragmentTransaction.replace(R.id.main_content, fragment, fragment.getFragmentTag());
    if (addToStack)
        fragmentTransaction.addToBackStack(fragment.getFragmentTag());
    fragmentTransaction.commit();
}

Don’t tell me you are not surprised this is simpler than you thought ! You already know how to add a fragment transaction, no need to explain much there. The juicy stuff is on detecting if the user is navigating to a section root fragment. If this is true we know we must clear everything up to our entry point (Inbox) and start from there. This is achieved using getSupportFragmentManager().popBackStackImmediate(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)

That line will clear the fragment’s back stack. But wait, if we are clearing the back stack, why do we see the Inbox fragment when going back ? Well, because we added this fragment inside onCreate() without recording anything on the back stack we are clearing 🙂

So far so good, but there is one last thing missing: what happens when removing a fragment from the back stack. We still need to interact with this event to properly update the UI:

@Override
public void onBackStackChanged() {
    MyAppFragment currentFragment = getCurrentFragment();
    if (currentFragment != null) {

        // since we are here, we can update the selected item on the sidebar
        setNavigationViewCheckedItem(currentFragment);

        if (currentFragment.getIsRootSection()) {
            showRootNavigation();
        } else {
            // this is not a "root" fragment. Use back arrow navigation on the app bar
            showBackArrowNavigation();
        }
    }
}

Easy right ? I bet you didn’t imagine this one as well. What we do here is: if the back stack was modified lets see where we got (current fragment != null). Based on this we change our UI.

Conclusion

There is always more and more to add about the Drawer pattern and fragment navigation since it’s basically our job to maintain it. I’m sure I will come back to this topic in the future.

Another thing I want to add is this is just a way on how to solve this issue. In a real “real” life scenario you will face login fragments, splash screens and shared elements. Solving that is quite a challenge too.

As a final word I want to say I really hope you find this useful, it’s been a lot of learning to me to achieve a consisten navigation, specially with the changes on the Android SDK over the years. I believe this approach can help a lot of developers building apps or even building better solutions ( please share !)

As usual, here is the code related to this article.

If you are done here yo may want to take a look at this post:

https://aarcoraci.wordpress.com/2017/02/16/android-application-drawer-dealing-with-configuration-changes/

Advertisements

2 thoughts on “Android Drawer and Fragment Navigation. A more “real life” scenario”

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s