Hybrid Xamarin.Forms Applications

When Xamarin.Forms was announced and released by Xamarin it represented an interesting take on their platform. The problem it attempted to solve was one that was becoming increasingly common in the mobile space and it represented a fair attempt to solve it. However, as a developer I do not consider Xamarin.Forms a replacement for the user of traditional Xamarin, just as I do not consider Xamarin a replacement for native development; all things have their places.

All that being said, Xamarin.Forms can give you some pretty cool advantages, one of which is the ability to design one code base and get decent rendering on multiple platforms. This makes it ideal for simple applications lacking significant customizations of the platform.

Most often, developers apply this advantage at a macro level, which is the designed purpose. However, if you consider that within complex applications you may have simple screens that do not gain any advantage from being developed in native code and that the potential of MIXING screens could yield a productivity benefit. This is what I have been looking at with Forms. I wanted to share with you what I found.

Understand the basic flow

Any mobile application represents an exercise in information architecture with the goal being to create the most efficient user flow possible. If you decide to mix screens, you have to really consider the order of your screens.

My advice for now is, assume that Xamarin.Forms will still control the navigation of your application at a high level. In some cases you may go from native page to native page, but even then, you want to do so while managing the Forms navigation flow. The rest of this article will assume this.

Custom Renderers

Each element that you see on your screen in Forms is drawn by a Renderer and one of the pillars of Forms customization is the notion of Custom Renderers whereby you can plug your own logic and perform custom rendering; this is the approach we will be using. When dealing with Custom Renderers you always want to define a custom control first. This will serve as a marker that will help to invoke your renderer.

public class NativePage : Page
{
     public string KeyName { get; set; }
     public string IosStoryboardName { get; set; }

     public NativePage()
     {
          IosStoryboardName = "Main";
     }
}

The properties on this class are used by the renderer to make sense for the various platforms:

  • KeyName – a unique name for the view or viewcontroller we will load for this page
  • StoryboardName – is specific to iOS and allow us to extract the related view controller out of a storyboard. The default of Main is used as the convention that most iOS developers use for their default storyboard.

For Xaml, we use this control as such:

<controls:NativePage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:controls="clr-namespace:NativeTransition" x:Class="NativeTransition.LoginPage" KeyName="Login">
</controls:NativePage>

By itself, this will do nothing. In fact, you could add normal content here and it would render as Forms normally does. This is because, by default, Xamarin.Forms registers a Renderer called PageRenderer for the Page class which is what does all of the magic. So, if Forms comes across a custom control it does the following:

  • Attempts to find a Renderer registered for its specific type name
  • Attempts to find a Renderer registered for its base class
  • Attempts to find a Renderer registered for its base class’ base class
  • And so on

So, our next step will be to define a Custom Renderer, and we should do this for each supported platform. If we dont, the above sequence will be applied.

About Custom Renderers (a few words)

Principally Custom Renderers are organized around two methods

  • OnElementChanged – called each time the primary element changes (that is it is initially created or a property is changed from its default value)
  • OnElementPropertyChanged – called each time the value of a property changes on an established control. This is used frequently to support custom data binding scenarios.

Generally, these methods are available in some form or another for each renderer you will create. The biggest point to understand about these methods, in particular with OnElementChanged, is that they are called multiple times. So, generally you want to setup some logic to ensure initial creation logic only runs one time.  Here is an example:

if (e.NewElement != null && e.OldElement == null)
{
     base.OnElementChanged(e);
     // Your Creation logic here
}

Having your creation logic inside this logic will ensure it is called one time and keep your rendering processing efficient.

The iOS Custom Renderer

With iOS, our goal is going to be to look at our KeyName and extract that ViewController from our storyboard. Once we have this ViewController we call some method to indicate to iOS that it is hosting our View Controller. As developers, we will be able to work with this View Controller as we would normally.

var keyName = (e.NewElement as NativePage)?.KeyName;
var storyboardName = (e.NewElement as NativePage)?.IosStoryboardName;
var viewController = BuildViewController(keyName, storyboardName);

// do the iOS thing to make it like the View Controller
ViewController.AddChildViewController(viewController);
ViewController.View.Add(viewController.View);				viewController.DidMoveToParentViewController(ViewController);

For simplicity, I have dropped my various null and parameter checks, but you will obviously want to code defensively here. The properties being used are defined on our custom Xamarin control in the XAML.

As for BuildViewController, its very simple and familiar to anyone who has done iOS for any amount of time.

UIViewController BuildViewController(string keyName, string storyboardName)
{
     var sourceStoryboard = UIStoryboard.FromName(storyboardName, null);
     var viewController = sourceStoryboard.InstantiateViewController(keyName);

     return viewController;
}

Once extracted, the view controller can be utilized as any normal View Controller in iOS would be, nothing special is required for Forms.

The Android Custom Renderer

Android is a bit more complicated, mainly because of how the windowing system works in general. Truthfully, Android is more well geared towards doing this sort of thing than iOS is, because we could have separate activities that represent different Forms applications. But like many things with Forms, it is made harder because you are abstract two different platforms. (If Xamarin would introduce a FormsViewController for iOS, I think the two could be made similar, but that is another conversation).

As we know, Android presents its view mainly though Xml layouts and Forms leverages a sort of dynamic view generation technique to accomplish this, I believe. In our case, we will want to use KeyName to dynamically load a view from our Resources folder. Here is the core logic:

var keyName = (e.NewElement as NativePage)?.KeyName;
var activity = Context as FormsAppCompatActivity;

_layoutView = BuildLayoutForKeyName(keyName);
var pageView = (LoginPageView)Activator.CreateInstance(typeof(LoginPageView));
AddView(_layoutView);

// support view interaction
pageView.PrepareView(
   activity, (e.NewElement as NativePage).Navigation, _layoutView);

So, this is where things get tricky because of the way Android is handled by Forms. In iOS, we get to work with full ViewController but in Android, everything is loaded into a single Activity, effectively replacing the ContentView each time, so we can expect to get an Android activity each time. This is the reason for the PageView concept which we will cover later.

Using the KeyName value, we attempt to find a layout whose name matches this value, inside BuildLayoutForKeyName.

Android.Views.View BuildLayoutForKeyName(string keyName)
{
     var id = Context.Resources.GetIdentifier(
          keyName.ToLower(), "layout", Context.PackageName);
     return (Context as Activity).LayoutInflater.Inflate(id, this, false);
}

Notice I have some leftover code as you would want to check for both forms of casing, not just assume lowercase, though you could as that is the standard in native Android.

The final bit is to adjust the size of the view as Android perform its layout operation, we do this using OnLayout lifecycle hook. As a note, I got this code off a Stackoverflow answer, but its pretty clear what it is doing.

protected override void OnLayout(bool changed, int l, int t, int r, int b)
{
     base.OnLayout(changed, l, t, r, b);
     var msw = MeasureSpec.MakeMeasureSpec(r - l, MeasureSpecMode.Exactly);
     var msh = MeasureSpec.MakeMeasureSpec(b - t, MeasureSpecMode.Exactly);

     _layoutView.Measure(msw, msh);
     _layoutView.Layout(0, 0, r - l, b - t);
}

Ok, using this approach will get our view to show up but, we have no way to interact with the View from the renderer, unless we create a dedicated renderer for each NativePage type which does not make a lot of sense. That is why I introduced the PageView concept.

The PageView

The idea behind PageView is not revolutionary, basically at a certain point we will pass off the generated View to a class whose sole purpose is to configure and setup the view. To be fair, there is a lot more we need to be able to do with Android views, so my approach here is still lacking, but its a fair start.

public class LoginPageView
{
     private FormsAppCompatActivity Activity { get; set; }
     private INavigation Navigation { get; set; }

     public void PrepareView(
          FormsAppCompatActivity activity, INavigation navigation,
          Android.Views.View view)
     {
          Activity = activity;
	  Navigation = navigation;

	  view.FindViewById<Android.Widget.Button>
               (Resource.Id.loginButton).Click += login_click;
     }

     void login_click(object sender, EventArgs e)
     {
     }
}

So, a few things that certainly need to change with this approach:

  • Its totally hard coded to LoginPageView. Since we cant use generics with renderers due to the export syntax not being amenable to it, we would need to specify it somewhere. My leading through is a convention based notation where I look for PageView and activate that.
  • It doesnt allow you to take advantage of the Android lifecycle methods. This is a big no no. My current leading thought is moving View inflation to this class as a base responsibility. But bringing the lifecycle methods over will remain tricky.

Definitely Android is a work in progress but, using this, you can create native Android interactions in your own custom View class without needing to reference Forms, though there is certainly more coupling with Forms here than there is in iOS.

Navigation back to Forms

So the cool thing here is, despite the fact that you are in native code, you are technically still in Forms, because all that is happening is Forms is rendering your Page as a native page. So, we can still use the standard Forms navigation techniques. For example, in Android you might do something like this:

async void login_click(object sender, EventArgs e)
{
     await Navigation.PushAsync(new PeopleListPage());
}

In iOS, you can do the above OR you can use traditional iOS navigation if you want (via PresentViewController). The reason this works is, under the hood all Forms does with each navigation is replace the RootViewController. Example:

partial void loginClick(NSObject sender)
{
     PresentViewController(
          new PeopleListPage().CreateViewController(), true, null);
}

To be fair, these are very simple scenarios that I have been testing, under normal application development, this may or may not work. But, remember, this sort of approach is designed to enable SOME screens to be Forms based and shared, not a large variety of them. Always consider your goal and what makes the most sense. Overusing this approach will lead you to problems.

Closing Notes

I want to once again reiterate that this approach should be used to augment your Traditional or Forms based applications. I do NOT advocate this being a 50/50 split, more like a 70/30 or 80/20. The navigation concerns alone suggest a high degree of caution when build apps with this feature.

The final note I would like to make is that this approach has gained a lot of popularity and Xamarin has taken notice. They have it tentatively planned to be in the 2.4 release of Forms. I say tentative because even they admit this is a hard problem to solve, but we all know it has some very strong advantages. It will be interesting to see how they support his use case, if they ever officially do.

Advertisement

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 )

Connecting to %s