Thursday, 30 November 2017

Xamarin.Forms 2.5 and Local Context on Android

Previously I wrote about Xamarin.Forms 2.5 and local context in Android custom renderers. I briefly mentioned that Xamarin.Forms.Forms.Context is now obsolete, and that in a custom renderer you can usually replace references to Xamarin.Forms.Forms.Context with a reference to the view’s context, which is passed into the custom renderer constructor.

So what do you do if your app references Xamarin.Forms.Forms.Context in a class that isn’t a custom renderer? For example, consider the GetVersionNumber method in the VersionHelper class:

using Android.Content; namespace DependencyServiceAndLocalContext.Droid { public class VersionHelper : IVersionHelper { public string GetVersionNumber() { var versionNumber = string.Empty; Context context = Xamarin.Forms.Forms.Context; if (context != null) { versionNumber = context.PackageManager.GetPackageInfo(context.PackageName, 0).VersionName; } return versionNumber; } } }

This code is executed by Xamarin.Forms DependencyService class, and references Xamarin.Forms.Forms.Context to get the local context. The problem is that as of Xamarin.Forms 2.5, it will result in a compiler warning - ‘Forms.Context’ is obsolete: ‘Context is obsolete as of version 2.5. Please use a local context instead.’

The solution is to get the local context without referencing Xamarin.Forms.Forms.Context. How this is achieved depends on whether your app has a single Activity, or multiple activities.

Note: my preferred approach is the technique documented for apps with multiple activities, as it’s also applicable to apps with only a single Activity. Therefore, this is the approach I’ve demonstrated in a sample app that can be found on GitHub.

Single Activity Apps

In a single Activity app the local context can be obtained from the MainActivity class. This can be achieved by adding an Init method to the VersionHelper class:

using Android.Content; namespace DependencyServiceAndLocalContext.Droid { public class VersionHelper : IVersionHelper { static Context _context; public static void Init(Context context) { _context = context; } public string GetVersionNumber() { var versionNumber = string.Empty; if (_context != null) { versionNumber = _context.PackageManager.GetPackageInfo( _context.PackageName, 0).VersionName; } return versionNumber; } } }

Then in the MainActivity class, call the VersionHelper.Init method, passing in the MainActivity instance as the context:

VersionHelper.Init(this);

Alternatively, create a static property in the MainActivity class, and set it to the MainActivity instance:

public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity { internal static MainActivity Instance { get; private set; } protected override void OnCreate(Bundle bundle) { ... global::Xamarin.Forms.Forms.Init(this, bundle); Instance = this; Xamarin.Forms.DependencyService.Register<IVersionHelper, VersionHelper>(); LoadApplication(new App()); } }

Then, in the VersionHelper class, retrieve the local context from the MainActivity class:

using Android.Content; namespace DependencyServiceAndLocalContext.Droid { public class VersionHelper : IVersionHelper { public string GetVersionNumber() { var versionNumber = string.Empty; if (MainActivity.Instance != null) { versionNumber = MainActivity.Instance.PackageManager.GetPackageInfo( MainActivity.Instance.PackageName, 0).VersionName; } return versionNumber; } } }

There are other variations on this theme, but they all amount to the same idea – retrieving the local context from the MainActivity class.

Multiple Activity Apps

If your app uses multiple activities, and hence you need to ensure that the local context is accurate, the approach outlined for single Activity apps can be used and modified, but it soon becomes messy to ensure that classes have references to the correct local context.

A better approach is to create a MainApplication class that implements the IActivityLifecycleCallbacks interface:

using System; using Android.App; using Android.Content; using Android.OS; using Android.Runtime; namespace DependencyServiceAndLocalContext.Droid { [Application] public partial class MainApplication : Application, Application.IActivityLifecycleCallbacks { internal static Context CurrentContext { get; private set; } public MainApplication(IntPtr handle, JniHandleOwnership transfer) : base(handle, transfer) { } public override void OnCreate() { base.OnCreate(); RegisterActivityLifecycleCallbacks(this); } public override void OnTerminate() { base.OnTerminate(); UnregisterActivityLifecycleCallbacks(this); } public void OnActivityCreated(Activity activity, Bundle savedInstanceState) { CurrentContext = activity; } public void OnActivityDestroyed(Activity activity) { } public void OnActivityPaused(Activity activity) { } public void OnActivityResumed(Activity activity) { CurrentContext = activity; } public void OnActivitySaveInstanceState(Activity activity, Bundle outState) { } public void OnActivityStarted(Activity activity) { CurrentContext = activity; } public void OnActivityStopped(Activity activity) { } } }

The CurrentContext property is updated with a reference to the current Activity, whenever an Activity is created, started, or resumed. Therefore this property will always have a reference to the current Activity, which can then be used as the local context. Therefore, the GetVersionNumber method in the VersionHelper class becomes:

using Android.Content; namespace DependencyServiceAndLocalContext.Droid { public class VersionHelper : IVersionHelper { public string GetVersionNumber() { var versionNumber = string.Empty; if (MainApplication.CurrentContext != null) { versionNumber = MainApplication.CurrentContext.PackageManager.GetPackageInfo( MainApplication.CurrentContext.PackageName, 0).VersionName; } return versionNumber; } } }

By using this approach, any references to Xamarin.Forms.Forms.Context can simply be replaced with MainApplication.CurrentContext. In addition, while this approach is aimed at apps that contain multiple activities, it has the advantage that it can also be used by apps that contain only a single Activity.

6 comments:

  1. Yes the multiple activity approach looks better.
    I have also seen this:
    Context context = Android.App.Application.Context;
    Is this a bad approach?

    ReplyDelete
    Replies
    1. Android.App.Application.Context returns the context of the single, global Application object of the current process, rather than the local context.

      The difference between the two? Local context (Activity's context) is tied to the lifecycle of the Activity, whereas the Application context is tied to the lifecycle of the Application.

      What does this mean in practice? Various things you try to do with the Application context will fail, particularly when related to the UI.

      The general rule of thumb is to use the local context, unless you need to save a reference to a context from an object that lives beyond your Activity. In which case use the Application context.

      A typical example of using the Application context is for starting an Activity e.g. Android.App.Application.Context.StartActivity(myIntent);

      Delete
    2. So going back to the VersionHelper example, the Application context would have sufficed. But that information is also attached to the local context, which is why it works.

      Delete
  2. What I can't understand in this obsolete change is - what is the benefit of replacing one (framework-side) global static with another (app-side)?

    ReplyDelete
  3. If I remember correctly, Forms.Context always referred to the MainActivity class in Forms apps.

    In Forms 2.5 there's a new feature called Forms embedding, which can be used to embed Forms pages into Xamarin.iOS/Xamarin.Android apps. Xamarin.Android apps tend to use multiple activities, rather than a single MainActivity class. Because of that, there was a danger that Xamarin.Android users would use Forms.Context to try and refer to the active Activity, but instead would get a reference to MainActivity, which would cause problems.

    So Forms.Context got obsoleted, and users have to handle the context themselves.

    I'm just the messenger :)

    ReplyDelete
    Replies
    1. I can understand that, but the question is, why no way in the android code to get the active Activity from Xamarin.Forms? Why do they force us to use the same obsolete static Forms.Context only declared outside of SDK?

      Delete