Auf plattformspezifische APIs in einer Xamarin.Forms-App zugreifen

Der Ansatz von Xamarin.Forms besteht darin, dass man fast die gesamte Anwendung nur einmal schreiben muss und diese App dann auf den verschiedenen mobilen Betriebssysteme wie Android und iOS zur Verfügung stehen. Dies bedeutet, dass man die meiste Zeit keinen plattformspezifischen Code schreiben müssen. Dies war jedoch nicht immer der Fall, da gerade zu Beginn der Xamarin.Forms-Entwicklung viel plattformspezifischer Code geschrieben werden musste, wenn man beispielsweise auf die Sensoren des Geräts zugreifen wollte. Aus diesem Grund gab es Plugins für Xamarin.Forms, die in den App-Projekten installiert werden können, um einen Wrapper für die plattformspezifischen APIs und die entsprechende Implementierung auf jeder Plattform zu erhalten. Bereits 2018 veröffentlichte Microsoft Xamarin.Essentials, ein NuGet-Paket, das jetzt Teil der Xamarin.Forms Templates ist, um den Zugriff auf viele plattformspezifische APIs zu ermöglichen, sodass man einfach über den plattformunabhängigen Code darauf zugreifen kann.

Diese Bibliothek deckt jedoch nicht alles ab, was bedeutet, dass man von Zeit zu Zeit auf plattformspezifische APIs zugreifen muss. In diesem Blog-Beitrag werde ich nun zeigen, wie wir einen eigenen Service schreiben können, um Toast-Benachrichtigungen auf Android und iOS anzuzeigen, und wie wir diesen Dienst mithilfe von Dependency Injection von unserem plattformunabhängigen Code aus aufrufen können.

Beginnen wir damit über das Interface für unseren ToastService nachzudenken, welches wir in das plattformunabhängige Projekt unserer Xamarin.Forms-App hinzufügen. In diesem Fall stellt unser Service nur eine Methode namens ShowToast mit zwei Parametern bereit. Der erste Parameter enthält die Nachricht und mit dem zweiten kann man die Dauer der angezeigten Zeit des Toasts ändern.

public interface IToastService
{
    void ShowToast(string message, bool isLongToast = false);
}

Nachdem wir nun das Interface angelegt haben, können wir mit dem Schreiben der plattformspezifischen Implementierungen beginnen. Wir beginnen mit Android. Dazu erstellen wir in dem Android-Projekt einen neuen Ordner Services und erstellen in diesem Ordner eine Klasse mit dem Namen ToastService. Dieser Service verwendet die Toast-Klasse von Android, um die entsprechende Meldung auf dem Screen anzuzeigen.

public class ToastService : IToastService
{
    private static Toast _toastInstance;

    public void ShowToast(string message, bool isLongToast = false)
    {
        var toastLength = isLongToast
            ? ToastLength.Long
            : ToastLength.Short;

        MainThread.BeginInvokeOnMainThread(() =>
        {
            _toastInstance?.Cancel();
            _toastInstance = Toast.MakeText(Application.Context, 
                message, toastLength);
            _toastInstance?.Show();
        });
    }
}

Für die iOS-Implementierung nutzen wir einen anderen Ansatz, denn das Konzept von einem Toast gibt es unter iOS so nicht. Dafür nutzen wir die Klasse UIAlertController und entfernen den Alert nach einer kurzen Zeit automatisch.

public class ToastService : IToastService
{
    private const double DelayLong = 5d;
    private const double DelayShort = 2d;

    public void ShowToast(string message, bool isLongToast = false)
    {
        var duration = isLongToast
            ? DelayLong
            : DelayShort;

        var alertController = UIAlertController.Create(null, 
            message, UIAlertControllerStyle.Alert);

        NSTimer.CreateScheduledTimer(duration, alertTimer =>
        {
            DismissToast(alertController, alertTimer);
        });

        UIApplication.SharedApplication.KeyWindow
            .RootViewController.PresentViewController(alertController, true, null);
    }

    private void DismissToast(UIAlertController alertController, NSTimer alertTimer)
    {
        alertController?.DismissViewController(true, null);
        alertTimer?.Dispose();
    }
}

Natürlich ist es möglich, den DependencyService von Xamarin.Forms zu verwenden. Aber meiner Meinung nach ist es nicht die beste architektonische Idee, diesen zu verwenden. Wir müssten die Services mit dem Dependency-Attribut markieren, das ein wenig an Performance kostet, da die App nach all diesen Attributen suchen und sie ordnungsgemäß registrieren muss.

Ich würde es vorziehen, einen anderen Service Locator ohne die Abhängigkeit von Xamarin.Forms zu verwenden. Es gibt verschiedene Pakete, aber ich verwende gerne Unity dafür, einen leichten, erweiterbaren Abhängigkeitsinjektionscontainer. Um Unity zu initialisieren, müssen wir dem plattformunabhängigen Projekt zwei NuGet-Pakete hinzufügen: Unity.Container und Unity.ServiceLocation. Nach der Installation erstellen wir eine neue Klasse namens Bootstrapper in unserem .NET Standard Projekt.

public static class Bootstrapper
{
    private static readonly UnityContainer _container = new UnityContainer();

    public static void RegisterDependencies()
    {
        // register platform independent services
        //_container.RegisterType<IService, Service>(
        //   new ContainerControlledLifetimeManager());

        // register viewmodels
        _container.RegisterType<MainPageViewModel>(
           new ContainerControlledLifetimeManager());

        var locator = new UnityServiceLocator(_container);
        ServiceLocator.SetLocatorProvider(() => locator);
    }

    public static void RegisterPlatformDependency<TInterface, TImplementation>(
        ITypeLifetimeManager lifetimeManager)
    {
        _container.RegisterType(typeof(TInterface), 
           typeof(TImplementation), lifetimeManager);
    }
}

Man kann sehen, dass wir einen Parameter an die RegisterType-Methode übergeben, um die Lebensdauer unserer Services oder ViewModels anzugeben. In diesem Fall verwenden wir einen Singleton-Ansatz, indem wir ContainerControlledLifetimeManager verwenden, um nur eine Instanz des Services bzw. ViewModels zu haben, wenn wir nun den ServiceLocator nach einer Instanz fragen, so erhalten wir diese. Es gibt verschiedene LifetimeManager, aber speziell für den Service halte ich es für einen guten Ansatz, mit Singleton zu arbeiten.

Im Bootstrapper stehen zwei Methoden zur Verfügung. Die Methode RegisterDependencies registriert alle plattformunabhängigen Services und ViewModels und setzt den ServiceLocator, damit wir über Dependency Injection auf diese Services zugreifen und ViewModels anzeigen können. Die andere Methode heißt RegisterPlatformDependency und muss aus den plattformspezifischen Projekten aufgerufen werden.

Normalerweise ruft man die RegisterDependencies-Methode in der Datei App.xaml.cs auf, bevor die MainPage-Property gesetzt wird. Die RegisterPlatformDependency-Methode wird für Android in der MainActivity-Klasse innerhalb der OnCreate-Methode aufgerufen.

protected override void OnCreate(Bundle savedInstanceState)
{
    TabLayoutResource = Resource.Layout.Tabbar;
    ToolbarResource = Resource.Layout.Toolbar;

    base.OnCreate(savedInstanceState);

    Xamarin.Essentials.Platform.Init(this, savedInstanceState);
    global::Xamarin.Forms.Forms.Init(this, savedInstanceState);
    Xamarin.Forms.FormsMaterial.Init(this, savedInstanceState);

    Bootstrapper.RegisterPlatformDependency<IToastService, ToastService>(
        new SingletonLifetimeManager());

    LoadApplication(new App());
}

Unter iOS rufen wir die Methode in der Klasse AppDelegate in der FinishedLaunching-Methode.

public override bool FinishedLaunching(UIApplication app, NSDictionary options)
{
    global::Xamarin.Forms.Forms.Init();
    Xamarin.Forms.FormsMaterial.Init();

    Bootstrapper.RegisterPlatformDependency<IToastService, ToastService>(
        new SingletonLifetimeManager());

    LoadApplication(new App());

    return base.FinishedLaunching(app, options);
}

Nachdem wir unseren Service mithilfe des Bootstrappers in unserem UnityContainer registriert haben, können wir nun ein ViewModel für die Verwendung unseres ToastService mithilfe von Dependency Injection erstellen.

Um die Nachricht in unserem Toast bearbeiten zu können, erstellen wir eine Property ToastMessage, die der Benutzer mit der entsprechenden Nachricht füllen kann. Das ViewModel bietet außerdem zwei Befehle, um entweder einen kurzen oder einen langen Toast anzuzeigen. Um Dependency Injection zu verwenden, verwenden wir einfach den Konstruktor und übergeben unser Interface IToastService.

public class MainPageViewModel : INotifyPropertyChanged
{
    private readonly IToastService _toastService;

    private string _toastMessage = "This is a toast message.";
    public string ToastMessage
    {
        get => _toastMessage;
        set { _toastMessage = value; OnPropertyChanged(); }
    }

    private ICommand _showShortToastCommand;
    public ICommand ShowShortToastCommand
        => _showShortToastCommand 
            ?? (_showShortToastCommand = new Command(ShowShortToast));

    private ICommand _showLongToastCommand;
    public ICommand ShowLongToastCommand
        => _showLongToastCommand 
            ?? (_showLongToastCommand = new Command(ShowLongToast));


    public MainPageViewModel(IToastService toastService)
    {
        _toastService = toastService;
    }

    private void ShowShortToast()
    {
        ShowToast(false);
    }

    private void ShowLongToast()
    {
        ShowToast(true);
    }

    private void ShowToast(bool isLongToast)
    {
        var message = string.IsNullOrEmpty(ToastMessage)
            ? "This is a toast message"
            : ToastMessage;

        _toastService.ShowToast(message, isLongToast);
    }


    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged([CallerMemberName]string name = "")
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
    }
}

Wir haben jetzt alles, um die App erstmalig zu starten. Für die UI verwende ich ein Entry, wo der Benutzer die Nachricht eingeben kann, die auf dem Toast angezeigt werden soll, sowie zwei Buttons, um entweder einen kurzen oder einen langen Toast anzuzeigen. Die folgende Animation zeigt die Implementierung unter Android.

Und hier das ganze noch einmal unter iOS.

You will find the source code in this GitHub repository.

In diesem Beitrag habe ich euch gezeigt, wie man mit Xamarin.Forms problemlos auf die nativen APIs zugreifen kann. Durch die Verwendung des Unity-Containers verfügen wir über eine saubere Architektur und mithilfe von Dependency Injection können wir problemlos auf die Methoden von unserem gemeinsam genutzten Code zugreifen.

Android NDK installieren Demo: Xamarin.Forms Material Visual DataTemplateSelector: Verschiedene DataTemplates für eine Liste