Blog Post

In my previous post, I talked about implementing an auto return toolbar using AppCompat and MvvmCross.

I indicated that it’s really important for MvvmCross to support toolbars in the layout and I went on to show some problems I’ve encountered with MvvmCross and toolbars in the layout in previous versions of the framework.

Today I’m going to show you another reason why you’d want to include a toolbar in the layout of an activity rather than letting the AppCompat theme do it for your automatically.

And this time it’s not to implement a feature as niche as an auto return toolbar. It’s to implement a feature that is becoming the defacto navigation standard for Material Design applications - the Navigation Drawer (a.k.a the hamburger menu).

Gmail Navigation Drawer

First, I’m going to implement a navigation drawer with standard Xamarin. After I get that working, I’ll add MvvmCross and implement each individual “page” as a view model.

So let’s go…

I’ve posted a sample project on GitHub so that you can follow along at home. I’ve created a branch called 1-darkactionbar-theme where I implement a basic navigation drawer without a toolbar in the layout. I’ll discuss why this isn’t an ideal solution in a few minutes.

This first implementation is pretty much a straight port right from the Android documentation.

First, add the Xamarin AppCompat package Xamarin.Android.Support.v7.AppCompat to the project.

Next, implement the AppCompat light theme with a dark action bar (that’s a colored action bar with white text):

<resources>
    <style name="MyTheme" parent="MyTheme.Base">
    </style>
    <style name="MyTheme.Base" parent="Theme.AppCompat.Light.DarkActionBar">
        <item name="colorPrimary">@color/primary</item>
        <item name="colorPrimaryDark">@color/primary_dark</item>
        <item name="colorAccent">@color/accent</item>
    </style>
</resources>

Following that, let’s layout the main activity:

<android.support.v4.widget.DrawerLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/drawerLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <!-- content, must go before menu because of z-order -->
    <FrameLayout
        android:id="@+id/frameLayout"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
    <!-- menu -->
    <ListView
        android:id="@+id/drawerListView"
        android:layout_gravity="start"
        android:choiceMode="singleChoice"
        android:layout_width="240dp"
        android:layout_height="match_parent"
        android:background="?android:attr/windowBackground" />
</android.support.v4.widget.DrawerLayout>

There’s a couple of important points to note about the layout:

  • The FrameLayout is where the content of each “page” will go. The pages will be implemented as fragments.

  • The ListView is the “drawer” that contains the menu items - it will slide in and out when the hamburger button is tapped

  • It is important that the ListView is defined after the FrameLayout because of the z-order of the controls. The menu must appear to be on top of the “page” when it is visible

  • The layout_gravity attribute positions the “drawer” on the screen - in this case start means “on the left” since Engish is a left to right language.

Finally, we’ll implement the main activity:

    [Activity (MainLauncher = true)]
    public class MainActivity : AppCompatActivity
    {
        Fragment[] _fragments = { new MyListFragment(), new MySettingsFragment() };

        string[] _titles = { "My List", "My Settings" };

        ActionBarDrawerToggle _drawerToggle;

        ListView _drawerListView;

        DrawerLayout _drawerLayout;

        protected override void OnCreate (Bundle savedInstanceState)
        {
            base.OnCreate (savedInstanceState);

            SetContentView (Resource.Layout.Main);

            SupportActionBar.SetDisplayHomeAsUpEnabled (true);

            _drawerListView = FindViewById<ListView> (Resource.Id.drawerListView);
            _drawerListView.ItemClick += (s, e) => ShowFragmentAt (e.Position);
            _drawerListView.Adapter = new ArrayAdapter<string> (
                this,
                global::Android.Resource.Layout.SimpleListItem1,
                _titles);

            _drawerLayout = FindViewById<DrawerLayout> (Resource.Id.drawerLayout);

            _drawerToggle = new ActionBarDrawerToggle (
                this,
                _drawerLayout,
                Resource.String.OpenDrawerString,
                Resource.String.CloseDrawerString);

            _drawerLayout.SetDrawerListener (_drawerToggle);

            ShowFragmentAt (0);
        }

        void ShowFragmentAt (int position)
        {
            SupportFragmentManager
                .BeginTransaction ()
                .Replace (Resource.Id.frameLayout, _fragments [position])
                .Commit ();

            Title = _titles [position];

            _drawerLayout.CloseDrawer (_drawerListView);
        }

        // snip

        public override bool OnOptionsItemSelected (IMenuItem item)
        {
            if (_drawerToggle.OnOptionsItemSelected (item))
                return true;

            return base.OnOptionsItemSelected (item);
        }
    }

A couple of points on the implementation:

  • The ActionBarDrawerToggle class is responsible for drawing the hamburger icon in the toolbar. It also changes the icon to a back arrow when the draw is opened.
  • At line 46, the fragment for the selected item in the drawer list view is added to the FrameLayout
  • At line 51, the drawer is closed when a menu item is selected
  • At line 58, the ActionBarDrawerToggle notifies the drawer layout to show the ListView when the button is tapped.

Some things haven’t been implemented, such as:

  • responding to screen rotation
  • closing the drawer when the back button is tapped
  • highlighting the selected row

I’ve tried to keep the code simple here so we can focus on the drawer and the toolbar. The Android documentation has the full implementation.

When I run the app the screen starts on the My List page:

My List Page

If I tap the hamburger icon the drawer expands and the hambuger icon changes to a back button:

Navigation Drawer Expanded

and if I select My Settings the current page is changed:

My Settings Page

There’s just one problem… Can you see it?

Compare the screenshot where the drawer is open to the Gmail screenshot at the top of the post.

In the Gmail app, the drawer covers the toolbar. In our implementation (and the Google sample) the drawer does not cover the toolbar.

Why’s that?

Toolbar Include

When the toolbar is shown as part of the AppCompat theme it is part of the activity decor. The toolbar is part of the screen chrome around the outside of the activity.

The items in the layout are all drawn inside the decor. In this case, it’s not possible for the drawer (that’s inside the layout) to cover the decor (that’s outside the layout).

To make the drawer behave as per the Navigation Drawer pattern, the toolbar should be moved within the layout of the activity.

The first step is to disable the automatic toolbar in my theme:

<resources>
    <style name="MyTheme" parent="MyTheme.Base">
    </style>
<!--<style name="MyTheme.Base" parent="Theme.AppCompat.Light.DarkActionBar">-->
    <style name="MyTheme.Base" parent="Theme.AppCompat.Light.NoActionBar">
        <item name="colorPrimary">@color/primary</item>
        <item name="colorPrimaryDark">@color/primary_dark</item>
        <item name="colorAccent">@color/accent</item>
        <item name="windowActionBar">false</item>
        <item name="android:windowNoTitle">true</item>
    </style>
</resources>

By setting the theme to NoActionBar, the toolbar won’t automatically be added to my activities.

Next, I create a layout called /Resources/layout/Toolbar.axml which will be my toolbar:

<android.support.v7.widget.Toolbar
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/toolbar"
    android:layout_width="match_parent"
    android:layout_height="?attr/actionBarSize"
    android:background="?attr/colorPrimary"
    app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
    app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" />

Next, include the toolbar layout in your activity layout files:

<android.support.v4.widget.DrawerLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/drawerLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <!-- content, must go before menu because of z-order -->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
        <include layout="@layout/toolbar" />
        <FrameLayout
            android:id="@+id/frameLayout"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    </LinearLayout>
    <!-- menu -->
    <ListView
        android:id="@+id/drawerListView"
        android:layout_gravity="start"
        android:choiceMode="singleChoice"
        android:layout_width="240dp"
        android:layout_height="match_parent"
        android:background="?android:attr/windowBackground" />
</android.support.v4.widget.DrawerLayout>

And don’t forget this important line:

    [Activity (MainLauncher = true)]
    public class MainActivity : AppCompatActivity
    {
        // snip

        protected override void OnCreate (Bundle savedInstanceState)
        {
            base.OnCreate (savedInstanceState);

            SetContentView (Resource.Layout.Main);

            var toolbar = FindViewById<Toolbar> (Resource.Id.toolbar);
            SetSupportActionBar (toolbar);

            SupportActionBar.SetDisplayHomeAsUpEnabled (true);

            // snip
        }

        // snip
    }

Now let’s if I run the app and expand the drawer:

Navigation Drawer Expanded

The drawer now completely covers the entire activity, just like it should.

You can see the full code in this commit of branch 2-include-toolbar.

MvvmCross

As I covered in my previous post, this wouldn’t be easily achievable with MvvmCross 3.5.1. That version of MvvmCross didn’t play nicely with AppCompat toolbars included in the layout.

However, with the release of MvvmCross 4.0 our AppCompat woes are behind us.

Let’s add MvvmCross to the project and implement the “pages” as view models.

Adding the NuGet package MvvmCross.Droid.Support.V7.AppCompat will add all the MvvmCross dependencies that I need.

Next up, I’ve created a PCL library to hold my view models. I have to add the package MvvmCross.Core to the PCL so that I can inherit my view models from MvxViewModel.

I’d like the menu options in the Navigation Drawer to come from the view model rather than be hard-coded in the Android activity:

public class MainViewModel : MvxViewModel
{
    readonly Type[] _menuItemTypes = {
        typeof(MyListViewModel),
        typeof(MySettingsViewModel),
    };

    public IEnumerable<string> MenuItems { get; private set; } = new [] { "My List", "My Settings" };

    public void ShowDefaultMenuItem()
    {
        NavigateTo (0);
    }

    public void NavigateTo (int position)
    {
        ShowViewModel (_menuItemTypes [position]);
    }
}

public class MenuItem : Tuple<string, Type>
{
    public MenuItem (string displayName, Type viewModelType)
        : base (displayName, viewModelType)
    {}

    public string DisplayName
    {
        get { return Item1; }
    }

    public Type ViewModelType
    {
        get { return Item2; }
    }
}

Notice that the navigation is now controlled by MvvmCross using the ShowViewModel API(at line 19).

By default, MvvmCross navigates between view models by changing activities. If I want to use fragments I need to change the default view presenter in Setup.cs:

public class Setup : MvxAndroidSetup
{
    // snip

    protected override IMvxAndroidViewPresenter CreateViewPresenter()
    {
        var mvxFragmentsPresenter = new MvxFragmentsPresenter(AndroidViewAssemblies);
        Mvx.RegisterSingleton<IMvxAndroidViewPresenter>(mvxFragmentsPresenter);
        return mvxFragmentsPresenter;
    }
}

Next up, I need to change the base class for my main activity:

[Activity]
public class MainActivity : MvxCachingFragmentCompatActivity<MainViewModel> // : AppCompatActivity
{
    // snip
}

I also need to provide some extra information on my fragments for MvvmCross:

[MvxFragmentAttribute(typeof(MainViewModel), Resource.Id.frameLayout)]
[Register("crossdrawer.android.MyListFragment")]
public class MyListFragment : MvxFragment<MyListViewModel> // : Fragment
{
    // snip
}

Let’s examine this in a bit more detail.

The MvxFragment attribute indicates to the fragment presenter:

  • in which activity this fragment will be hosted (MainViewModel, and by association MainActivity)
  • where it will be placed in that activity (in a FrameLayout with the id frameLayout)

Also note the Register attribute on the fragment - this is required otherwise the app will crash.

MvvmCross instantiates the fragement with a call to Fragment.Instantiate(). This requires the Java class name of the fragemnt to create.

Unfortunately, Xamarin doesn’t register activities and fragments with the Java OS with names as you’d expect - it adds an MD5 hash to the name by default, as described here.

So we need to explicity use Register here to force the runtime to register the name as crossdrawer.android.MyListFragment. Notice the lowercase of the namespace.

That’s it! You can see the full code in this commit in master.

Wrap Up

With the release of MvvmCross 4.0, it’s now very easy to create an app with a navigation drawer.

We can use view models for all of the “pages” in the application, using the goodness of databinding to move the data back and forth from view to view model without writing code in the view.

Additionally, the menu items in the navigation drawer could also be bound to the listview from the view model, creating a easily unit testable, non-platform specific implementation of the menu.