Utilising Autofill in an Xamarin Forms Android App

What is Android Autofill?

Account creation, login, and credit card transactions take time and are prone to errors. Users can easily get frustrated with apps that require these types of repetitive tasks. Android 8.0 (API level 26) makes filling out forms, such as login and credit card forms, easier with the introduction of the Autofill Framework.

The framework is divided into two parts; Apps that can store autofill data (such as password managers or banking apps), and apps that can consume the data of a user's chosen Autofill service.

Xamarin Forms Effects & Platform Specifics

Back in the early days of Xamarin Forms, if you wanted to extend the functionality of an Element to provide additional properties to its underlying native control, you had to create Custom Renderers to replace substantial parts of the logic, sometimes with unexpected consequences.

Fast forward to mid-2016 and Xamarin introduced a new way to extend a native control's functionality: Effects. These allow the native controls on each platform to be customized in a similar way to Custom Renderers, but with much less faff. In most cases, if you only want to add a couple of minor styling changes to a control, they're the recommended approach.

As the Framework continues to evolve, Xamarin keep improving the ways in which developers can add custom functionality. Platform Specifics provide a method of accessing features unique to certain platforms by utilising attached properties, which can be specified in Xaml or Code-behind.

Adding an Autofill Effect into your app

In order to incorporate the Autofill feature into an app, we'll first start with creating the PlatformSpecifics class in our Shared project which provide the AttachedProperties and store the Autofill Type value for each control. Then we'll build an Effect, which we'll utilise to get the value from the AttachedProperty and use it to modify the Control.

A full sample of the code can be found here: GitHub

AutofillHintTypes

The Autofill service works by matching keys which are stored as strings. Android recommend using the constants provided by the View, however, as these are specific to Android, we'll create an enum which can expose these within the Shared Project.

public enum AutofillHintType
{
    [Description("")]
    None,
    [Description("creditCardExpirationDate")]
    CreditCardExpirationDate,
    [Description("creditCardExpirationDay")]
    CreditCardExpirationDay,
    [Description("creditCardExpirationMonth")]
    CreditCardExpirationMonth,
    [Description("creditCardExpirationYear")]
    CreditCardExpirationYear,
    [Description("creditCardNumber")]
    CreditCardNumber,
    [Description("creditCardSecurityCode")]
    CreditCardSecurityCode,
    [Description("emailAddress")]
    EmailAddress,
    [Description("name")]
    Name,
    [Description("password")]
    Password,
    [Description("phone")]
    Phone,
    [Description("postalAddress")]
    PostalAddress,
    [Description("postalCode")]
    PostalCode,
    [Description("username")]
    Username
}

We utilise the System.ComponentModel.DescriptionAttribute to specify the constant value consistent with Android.

Autofill PlatformSpecifics

Next we'll add an AttachedProperty that can be used by the view and hook up its OnChanged event to add or remove the AutofillEffect (that we'll create later) to the Native Android control.

namespace XamAutofill.PlatformConfiguration.AndroidSpecific
{
    using System.Linq;
    using Xamarin.Forms;
    using FormsElement = Xamarin.Forms.Entry;

    public static class Entry
    {
        const string EffectName = "com.adenearnshaw.AutofillEffect";

        public static readonly BindableProperty AutofillHintProperty =
            BindableProperty.CreateAttached(nameof(AutofillHint),
                                            typeof(AutofillHintType),
                                            typeof(Entry),
                                            AutofillHintType.None,
                                            propertyChanged: OnAutofillHintPropertyChanged);

        public static AutofillHintType GetAutofillHint(BindableObject element)
        {
            return (AutofillHintType)element.GetValue(AutofillHintProperty);
        }

        public static void SetAutofillHint(BindableObject element, AutofillHintType value)
        {
            element.SetValue(AutofillHintProperty, value);
        }

        private static void OnAutofillHintPropertyChanged(BindableObject element, object oldValue, object newValue)
        {
            if ((AutofillHintType)newValue != AutofillHintType.None)
            {
                AttachEffect(element as FormsElement);
            }
            else
            {
                DetachEffect(element as FormsElement);
            }
        }

        private static void AttachEffect(FormsElement element)
        {
            IElementController controller = element;
            if (controller == null || controller.EffectIsAttached(EffectName))
            {
                return;
            }
            element.Effects.Add(Effect.Resolve(EffectName));
        }

        private static void DetachEffect(FormsElement element)
        {
            IElementController controller = element;
            if (controller == null || !controller.EffectIsAttached(EffectName))
            {
                return;
            }

            var toRemove = element.Effects.FirstOrDefault(e => e.ResolveId == Effect.Resolve(EffectName).ResolveId);
            if (toRemove != null)
            {
                element.Effects.Remove(toRemove);
            }
        }
    }
}

Now that we have the attached property, we'll add the second half of this class which is a couple of Extension methods which allow the AttachedProperty to be accessed via the PlatformConfiguration Fluent API.

namespace XamAutofill.PlatformConfiguration.AndroidSpecific
{
    using System.Linq;
    using Xamarin.Forms;
    using Xamarin.Forms.PlatformConfiguration;
    using FormsElement = Xamarin.Forms.Entry;

    public static class Entry
    {
        const string EffectName = "com.adenearnshaw.AutofillEffect";

        ...

        public static AutofillHintType AutofillHint(this IPlatformElementConfiguration<Android, FormsElement> config)
        {
            return GetAutofillHint(config.Element);
        }

        public static IPlatformElementConfiguration<Android, FormsElement> SetAutofillHint(this IPlatformElementConfiguration<Android, FormsElement> config, AutofillHintType value)
        {
            SetAutofillHint(config.Element, value);
            return config;
        }
    }
}

AutofillEffect

Now we have our PlatformSpecifics code, we'll add in the AutofillEffect so that we can apply our property to the Native control.

First, create a RoutingEffect in the Shared code. This is a shell, and allows the Shared code a link to the Platform's Effect that we'll create next.

namespace XamAutofill
{
    public class AutofillEffect : RoutingEffect
    {
        public AutofillEffect() : base("com.adenearnshaw.AutofillEffect")
        {
        }
    }
}

Second is to create the PlatformEffect. This is created in the Android Project and contains the logic to apply the property to the native control.

using Xamarin.Forms.Platform.Android;
using Xamarin.Forms;
using System;
using System.Reflection;
using System.ComponentModel;
using XamAutofill.PlatformConfiguration.AndroidSpecific;
using Entry = Xamarin.Forms.Entry;

[assembly: ResolutionGroupName("com.adenearnshaw")] // Remember, only set this on one effect within your App, otherwise conflicts can occur.
[assembly: ExportEffect(typeof(XamAutofill.Droid.AutofillEffect), "AutofillEffect")]
namespace XamAutofill.Droid
{
    public class AutofillEffect : PlatformEffect
    {
        protected override void OnAttached()
        {
            try
            {
                if (!IsSupported())
                    return;

                var control = Control as Android.Widget.EditText;

                var hint = GetAndroidAutofillHint();
                control.ImportantForAutofill = Android.Views.ImportantForAutofill.Yes;
                control.SetAutofillHints(hint);
            }
            catch (Exception ex)
            {
                Console.WriteLine("Cannot set property on attached control. Error: {0}", ex.Message);
            }
        }

        protected override void OnDetached()
        {
            if (!IsSupported())
                return;

            Control.SetAutofillHints(string.Empty);

            Control.ImportantForAutofill = Android.Views.ImportantForAutofill.Auto;
        }

        private bool IsSupported()
        {
            return Element as Entry != null && 
                   Android.OS.Build.VERSION.SdkInt >= Android.OS.BuildVersionCodes.O;
        }

        private string GetAndroidAutofillHint()
        {
            var hint = ((Entry)Element).OnThisPlatform().AutofillHint();
            var constantValue = GetEnumDescription(hint);
            return constantValue;
        }

        private static string GetEnumDescription(Enum value)
        {
            FieldInfo fi = value.GetType().GetField(value.ToString());

            DescriptionAttribute[] attributes =
                (DescriptionAttribute[])fi.GetCustomAttributes(typeof(DescriptionAttribute), false);

            return attributes != null && attributes.Length > 0 ? attributes[0].Description : value.ToString();
        }
    }
}

When the AttachedProperty in the SharedCode is declared and has a value other than AutofillHintType.None, then an Effect is attached to the Control. This then does the following:

  1. The PlatformEffect's OnAttached method checks to see if Autofill is supported
  2. Then gets the enum value from the AutofillHint() extension method declared earlier and uses reflection to get the constant declared in the DescriptionAttribute.
  3. The constant is then applied to the Control using the EditText.SetAutofillHints() method.

The EditText.ImportantForAutofill property is also set to force the Autofill service to lookup and insert a value for the field.

Consuming the code

Right, we've got all the parts we need, we just need to use them now. Below are two examples of how the PlatformSpecifics can be used. In XAML, declare the namespace for at the top of the file, then add the AttachedProperty to your Entry element. In Code-Behind, use the OnPlatform Fluent API to set the property using the Extension method

XAML

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:android="clr-namespace:XamAutofill.PlatformConfiguration.AndroidSpecific;assembly=XamAutofill"
             x:Class="XamAutofill.MainPage" >
    <StackLayout Margin="12,12,12,12">
        <Entry HorizontalOptions="Fill"
               Placeholder="Your name"
               Keyboard="Text"
               android:Entry.AutofillHint="Name"/>
        <Entry x:Name="PhoneEntry"
               HorizontalOptions="Fill"
               Placeholder="Your phone"
               Keyboard="Telephone"/>
    </StackLayout>
</ContentPage>

Code-behind

public partial class MainPage : ContentPage
{
    public MainPage()
    {
        InitializeComponent();

        PhoneEntry.On&lt;Android&gt;().SetAutofillHint(AutofillHintType.Phone);
    }
}

A full sample of the code can be found here: GitHub

More information on creating Effects & Platform specifcs:
Creating Effects: docs.microsoft.com/en-us/xamarin/xamarin-forms/app-fundamentals/effects/creating
Creating PlatformSpecifics: docs.microsoft.com/en-us/xamarin/xamarin-forms/platform/platform-specifics/creating
Consuming PlatformSpecifics: docs.microsoft.com/en-us/xamarin/xamarin-forms/platform/platform-specifics/consuming/android
Testing Autofill: developer.xamarin.com/samples/monodroid/android-o/AutofillFramework/