Thursday, June 06, 2013

More dynamic Android Fragments with MVVM and MVVMCross

 

Scenario: I need to create and load fragments dynamically (by code).

Example: I have a hierarchical data with categories and items, a category can contain items and other categories, like in Windows Explorer:

image

When user clicks on a folder icon, if it’s a folder, it will show the same view with folder’s child folders and items. If it’s an icon, we want to show just a label with the name of the item (in real-life, this can be rendering a document).

In Android, we will need

  1. an activity class and layout (or even a parent fragment) as host for the views. it will create and show the fragments dynamically by code,
  2. a fragment class and layout for the folder view (with a GridView)
  3. a fragment class and layout for the item view (with a TextView)

With MVVMCross, we will also have:

  1. a view-model class for the activity/parent fragment host
  2. a view-model class for the folder view
  3. a view-model class for the item view

With MVVM, when user clicks on a folder icon, the view-model is responsible to handle the event (by a Command) and trigger navigation to another fragment view, depending on the icon. But remember that the view-model must NOT directly instantiate and show the views, this is platform specific UI code.

In MVVMCross, we call MvxViewModel.ShowViewModel method to‘show’ the view-model. By default, what happens is, the MVVMCross finds the associated activity class (based on a name convention) and shows it. But In our case, this won’t work, because the view-models refer to fragments.

The solution is to use MVVMCross features to control the view creation mechanism.

MVVMCross has a built-in MvxAndroidViewPresenter class used to create and show an activity based on the view-model:

	public class MvxAndroidViewPresenter : IMvxAndroidViewPresenter, IMvxViewPresenter
{
public virtual void Show (MvxViewModelRequest request)
{
IMvxAndroidViewModelRequestTranslator mvxAndroidViewModelRequestTranslator = Mvx.Resolve<IMvxAndroidViewModelRequestTranslator> ();
Intent intentFor = mvxAndroidViewModelRequestTranslator.GetIntentFor (request);
this.Activity.StartActivity (intentFor);
}
}

We can tell MVVMCross to use instead our own view presenter implementation which creates and shows fragments inside our activity or parent fragment.

	public class CustomPresenter : MvxAndroidViewPresenter, ICustomPresenter
{
// map between view-model and fragment host which creates and shows the view based on the view-model type
private Dictionary<Type, IFragmentHost> dictionary = new Dictionary<Type, IFragmentHost>();

public override void Show(MvxViewModelRequest request)
{
IFragmentHost host;
if (this.dictionary.TryGetValue(request.ViewModelType, out host))
{
if (host.Show(request))
{
return;
}
}
base.Show(request);
}

public void Register(Type viewModelType, IFragmentHost host)
{
this.dictionary[viewModelType] = host;
}
}

The class is responsible for keeping a mapping between view-model types and the hosts which will instantiate and show the fragments.


We can now can register it for MVVMCross to use it, this being done in the Setup class in the Android app:

public class Setup : MvxAndroidSetup
{
protected override IMvxAndroidViewPresenter CreateViewPresenter()
{
var customPresenter = new CustomPresenter();
Mvx.RegisterSingleton<ICustomPresenter>(customPresenter);
return customPresenter;
}
}

Note that the custom presenter once registered in MVVMCross, will be called for ANY view-model, whenever we call MvvmViewModel : ShowViewModel<T>();


ICustomPresenter and IFragmentHost are some simple interfaces we define in our code in order to nicely decouple the MVVMCross custom view presenter from the fragment host:

public interface ICustomPresenter
{
void Register(Type viewModelType, IFragmentHost host);
}

public interface IFragmentHost
{
bool Show(MvxViewModelRequest request);
}

It makes sense for the activity class to implement the IFragmentHost interface:

public class MainView: MvxFragmentActivity, IFragmentHost
{
public bool Show(Cirrious.MvvmCross.ViewModelsMvxViewModelRequest request)
{
// create view model
var loaderService = Mvx.Resolve<IMvxViewModelLoader> ();
var viewModel = loaderService.LoadViewModel (request, null /* saved state */);

// decide which fragment to create based on the view-model type
var fragmentView = ...

// load fragment into view
var ft = fragmentManager.BeginTransaction ();
ft.Replace (Resource.Id.fragmentHost, fragmentView);
ft.AddToBackStack (null);
ft.Commit ();
}
}

Further more, we can have the activity have its own dictionary to keep a mapping between view-models and fragments, such that we can instantiate fragments based on the view-model class:

		// map between view-model and fragment which creates and shows the fragment based on the view-model type
private Dictionary<Type, Type> vmTypeFragmentTypeDictionary = new Dictionary<Type, Type>();

public bool ShowFragment(FragmentManager fragmentManager, MvxViewModelRequest request, bool addToBackstack)
{
Type fragmentType;
if (vmTypeFragmentTypeDictionary.TryGetValue (request.ViewModelType, out fragmentType)) {

// create view model
var loaderService = Mvx.Resolve<IMvxViewModelLoader> ();
var viewModel = loaderService.LoadViewModel (request, null /* saved state */);

// create fragment view and bind it to the view model
var fragmentView = Activator.CreateInstance (fragmentType) as MvxFragment;
fragmentView.ViewModel = viewModel;

// load fragment into view
var ft = fragmentManager.BeginTransaction ();
ft.Replace (Resource.Id.fragmentHost, fragmentView);
if (addToBackstack) {
ft.AddToBackStack (null);
}
ft.Commit ();
return true;
}
else {
return false;
}
}

public void Register(Type viewModelType, Type fragmentType)
{
vmTypeFragmentTypeDictionary[viewModelType] = fragmentType;
}


To make it reusable, we can even create a base class with this code, called for example MvxActivityFragmentHost and have the activity derive from it:

public class MvxActivityFragmentHost : MvxFragment, IFragmentHost
{
...
}

public class MainView : MvxActivityFragmentHost
{
....
}

8 comments:

  1. Anonymous11:55 PM

    Hello!

    Could you publish the cource code of this particular sample, please?

    ReplyDelete
  2. Anonymous11:01 AM

    This is really neat. Thanks for the efforts. One question though: when to call register method?

    ReplyDelete
  3. Anonymous5:49 AM

    Hi!

    can you please publish source code?

    Best regards

    ReplyDelete
  4. Anonymous6:54 AM

    Hallo!

    can you please publish source code?

    Best regards

    ReplyDelete
  5. Hi. And what should I do, if I want to load fragment activity with fragment. So now I pass parameter to activity view model with type of fragment view model. When activity view model is loaded and parameter is passed, call ShowViewModel another time with Type parameter. Can we just call fragment view model?
    Best regards

    ReplyDelete
  6. Why does the host activity inherit from MvxActivityFragmentHost which inherits from MvxFragment?

    An activity isn't a fragment right?

    ReplyDelete
  7. couldnt find a better name.
    MvxActivityFragmentHost is an activity which is a host for fragments

    ReplyDelete
  8. Patrick Long1:17 AM

    Did you do a Windows Version of this app? Obviously it does not have fragments so did you switch to USerControls?

    ReplyDelete