Presenters in MvvmCross: Navigating Android with Fragments

In my last couple posts on this subject I've mentioned using fragments on Android for navigation. By default MvvmCross will use activities on Android, but as I hope you've learned by now, you can use presenters to customize this sort of thing as much as you'd like. I've found it very difficult (or impossible) to properly achieve the types of fine-grained navigation control I want in my apps by using activities for all my views, which is one of several reasons I've switched to using fragments for everything.

Ultimately what this ends up looking like is somewhat similar to a Single Page Application on the web: a single container activity with a fragment stack containing the actual views of the app. You can mix and match here if you'd like too, as I'll show later in my sample presenter.

Views and View Models

First let's quickly set up the basic app essentials here, starting with the view models:

using Cirrious.MvvmCross.ViewModels;

namespace PresenterDemo.Core.ViewModels  
{
    public class FirstViewModel : MvxViewModel
    {
        public IMvxCommand NavigateCommand
        {
            get { return new MvxCommand(() => ShowViewModel<SecondViewModel> ()); }
        }
    }

    public class SecondViewModel : MvxViewModel
    {
    }
}

Nothing crazy here, just a view model that can navigate to a second view model. Next we'll define the view markup, starting with FirstView.axml:

<?xml version="1.0" encoding="utf-8"?>  
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
    xmlns:local="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Navigate"
        local:MvxBind="Click NavigateCommand" />
</LinearLayout>  

and SecondView.axml:

<?xml version="1.0" encoding="utf-8"?>  
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
    xmlns:local="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Second view" />
</LinearLayout>  

As I mentioned earlier, the app will use a single activity that hosts the fragments, so we'll also define a layout named Container.axml:

<?xml version="1.0" encoding="utf-8"?>  
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
    xmlns:local="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    <FrameLayout
        android:id="@+id/contentFrame"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>  

Finally we need our fragments:

using Android.OS;  
using Android.Views;  
using Cirrious.MvvmCross.Binding.Droid.BindingContext;  
using Cirrious.MvvmCross.Droid.FullFragging.Fragments;

namespace PresenterDemo.Droid  
{
    public class InitialFragment : MvxFragment
    {
        public override View OnCreateView (LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
        {
            base.OnCreateView(inflater, container, savedInstanceState);

            return this.BindingInflate(Resource.Layout.FirstView, null);
        }
    }

    public class SecondView : MvxFragment
    {
        public override View OnCreateView (LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
        {
            base.OnCreateView(inflater, container, savedInstanceState);

            return this.BindingInflate(Resource.Layout.SecondView, null);
        }
    }
}

I'll get back to the activity itself a little later.

Fragment Lookup

As with most things there are an endless number of ways you can implement this sort of thing, but in this example we're going to create a class that scans the assembly for fragment views and create a mapping that can be used to match them up with view models by name. This will follow the standard convention of matching a FooViewModel view model with a view named FooView.

using System;  
using System.Collections.Generic;  
using System.Linq;  
using Cirrious.CrossCore.IoC;  
using Cirrious.MvvmCross.Droid.FullFragging.Fragments;

namespace PresenterDemo.Droid  
{
    public interface IFragmentTypeLookup
    {
        bool TryGetFragmentType(Type viewModelType, out Type fragmentType);
    }

    public class FragmentTypeLookup : IFragmentTypeLookup
    {
        private readonly IDictionary<string, Type> _fragmentLookup = new Dictionary<string, Type>(); 

        public FragmentTypeLookup()
        {
            _fragmentLookup = 
                (from type in GetType().Assembly.ExceptionSafeGetTypes ()
                 where !type.IsAbstract 
                    && !type.IsInterface 
                    && typeof(MvxFragment).IsAssignableFrom(type)
                    && type.Name.EndsWith("View")
                 select type).ToDictionary(getStrippedName);
        }

        public bool TryGetFragmentType(Type viewModelType, out Type fragmentType)
        {
            var strippedName = getStrippedName(viewModelType);

            if (!_fragmentLookup.ContainsKey(strippedName))
            {
                fragmentType = null;

                return false;
            }

            fragmentType = _fragmentLookup[strippedName];

            return true;
        }

        private string getStrippedName(Type type)
        {
            return type.Name
                       .TrimEnd("View".ToCharArray())
                       .TrimEnd("ViewModel".ToCharArray());
        }
    }
}

The Presenter

With that mapping implemented, now we can actually work on the presenter itself. This implementation will keep things simple, but you could easily extend it with fancier navigation patterns like the ones shown in previous posts here. Let's start with the basic outline for the class:

using System;  
using Android.App;  
using Cirrious.MvvmCross.Droid.FullFragging.Fragments;  
using Cirrious.MvvmCross.Droid.Views;  
using Cirrious.MvvmCross.ViewModels;

namespace PresenterDemo.Droid  
{
    public class DroidPresenter : MvxAndroidViewPresenter
    {
        private readonly IMvxViewModelLoader _viewModelLoader;
        private readonly IFragmentTypeLookup _fragmentTypeLookup;
        private FragmentManager _fragmentManager;

        public DroidPresenter(IMvxViewModelLoader viewModelLoader, IFragmentTypeLookup fragmentTypeLookup)
        {
            _fragmentTypeLookup = fragmentTypeLookup;
            _viewModelLoader = viewModelLoader;
        }
    }
}

The presenter takes in the fragment mapping and a view model loader as dependencies, which can be used later during navigation to inflate the proper fragments.

Now, this next part is the one thing here that feels somewhat dirty, but its scope is pretty limited. Since the container activity is what is going to be started initially, we'll make it so that activity has some knowledge of the presenter, and registers its fragment manager and an initial fragment to show with the presenter:

public void RegisterFragmentManager(FragmentManager fragmentManager, MvxFragment initialFragment)  
{
    _fragmentManager = fragmentManager;

    showFragment(initialFragment, false);
}

This isn't quite as clean of a separation as you get on iOS, but it's not bad. Next we'll handle when a new view model request comes in:

public override void Show(MvxViewModelRequest request)  
{
    Type fragmentType;
    if (_fragmentManager == null || !_fragmentTypeLookup.TryGetFragmentType(request.ViewModelType, out fragmentType))
    {
        base.Show(request);

        return;
    }

    var fragment = (MvxFragment)Activator.CreateInstance(fragmentType);
    fragment.ViewModel = _viewModelLoader.LoadViewModel(request, null);

    showFragment(fragment, true);
}

The main thing to notice here is that if no corresponding fragment is found in the map, we fall back to the base presenter's implementation. This means that if a request comes in for ThirdViewModel, it will actually go look for an activity named ThirdView, since it won't match anything in the fragment type map. This allows you to switch between activities and fragments if you wish.

Both of these last two methods call a private method to actually show the fragment:

private void showFragment(MvxFragment fragment, bool addToBackStack)  
{
    var transaction = _fragmentManager.BeginTransaction();

    if (addToBackStack)
        transaction.AddToBackStack(fragment.GetType().Name);

    transaction
        .Replace(Resource.Id.contentFrame, fragment)
        .Commit();
}

For the initial fragment we don't want to add it to the backstack, because then you'd be able to hit back on the initial view and see an empty screen, rather than back out of the app as expected. This is also where you could add things like transition animations if you wanted.

Finally, we need to handle when Close requests come in:

public override void Close(IMvxViewModel viewModel)  
{
    var currentFragment = _fragmentManager.FindFragmentById(Resource.Id.contentFrame) as MvxFragment;
    if (currentFragment != null && currentFragment.ViewModel == viewModel)
    {
        _fragmentManager.PopBackStackImmediate();

        return;
    }

    base.Close(viewModel);
}

Again, this implementation allows for mixing fragments and activities. If the request comes in from the current fragment it will be popped off the stack, but if not it will defer to the base implementation to close the view model.

Wiring It Up

Now all that's left is to wire up our dependencies and connect the activity to the presenter:

using Android.Content;  
using Cirrious.CrossCore;  
using Cirrious.MvvmCross.Droid.Platform;  
using Cirrious.MvvmCross.Droid.Views;  
using Cirrious.MvvmCross.ViewModels;

namespace PresenterDemo.Droid  
{
    public class Setup : MvxAndroidSetup
    {
        public Setup(Context applicationContext) : base(applicationContext)
        {
        }

        protected override IMvxApplication CreateApp()
        {
            return new Core.App();
        }

        protected override IMvxAndroidViewPresenter CreateViewPresenter()
        {
            var presenter = Mvx.IocConstruct<DroidPresenter>();

            Mvx.RegisterSingleton<IMvxAndroidViewPresenter>(presenter);

            return presenter;
        }

        protected override void InitializeIoC()
        {
            base.InitializeIoC();

            Mvx.ConstructAndRegisterSingleton<IFragmentTypeLookup, FragmentTypeLookup>();
        }
    }
}

With that in place we can create our container activity and connect the presenter:

using Android.App;  
using Android.OS;  
using Cirrious.CrossCore;  
using Cirrious.MvvmCross.Droid.Views;

namespace PresenterDemo.Droid.Views  
{
    [Activity(Label = "Presenter Demo", 
              MainLauncher = true,
              Icon = "@drawable/icon")]
    public class FirstView : MvxActivity
    {
        protected override void OnCreate(Bundle bundle)
        {
            base.OnCreate(bundle);

            SetContentView(Resource.Layout.Container);

            var presenter = (DroidPresenter)Mvx.Resolve<IMvxAndroidViewPresenter>();
            var initialFragment = new InitialFragment { ViewModel = ViewModel };

            presenter.RegisterFragmentManager(FragmentManager, initialFragment);
        }
    }
}

In this example we simply pass the view model from the activity into the first fragment, but you could customize this as necessary of course.

That's all you need to set up basic fragment-based navigation in your Android apps. I highly recommend using fragments when possible, both for the flexibility around navigation and also just for the ability for reuse in different layout configurations. There are a number of different approaches to fragment presenters out there, but I wanted to share one approach I've found that has worked very well in my apps and has given me a lot of flexibility.

comments powered by Disqus
Navigation