Skip to content

Instantly share code, notes, and snippets.

@followthatleader
Last active September 21, 2017 14:56
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save followthatleader/eb239699ba9292d003e9101c4aa9023f to your computer and use it in GitHub Desktop.
Save followthatleader/eb239699ba9292d003e9101c4aa9023f to your computer and use it in GitHub Desktop.
Presenters in MvvmCross: Navigating Android with Fragments (Greg Shackles)
// Greg's awesome article had broken syntax highligting, leading to unreadable code :( So I fixed it up here,
// putting it on a Gist for myself and for others. Here's the original article:
// http://www.gregshackles.com/presenters-in-mvvmcross-navigating-android-with-fragments/
// 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:
// [See the article for this]
// 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:
// [See the article for this]
// 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment