zurück zum Artikel

Erfahrungen aus der Cross-Plattform-Entwicklung mit Xamarin – die Übersicht

Simon André Scherr, Steffen Hupp, Patrick Mennig
Erfahrungen aus der Cross-Plattform-Entwicklung mit Xamarin – die Übersicht

(Bild: BEST-BACKGROUNDS / Shutterstock.com)

Xamarin versucht, durch native Programmierung plattformübergreifend zu agieren. Ein Artikel über Potenziale, Architektur und Implementierungsdetails des Tools.

Xamarin entstammt der Mono-Community, die sich zum Ziel gesetzt hatte, ein Open-Source-Pendant zu Microsofts damals proprietärem .NET Framework zu entwickeln. Es soll die Entwicklung von Cross-Plattform-Applikationen im mobilen Bereich vereinfachen und beschleunigen. Microsoft übernahm 2016 Xamarin, was dazu führte, dass der Konzern das bis dahin kostenpflichtige Werkzeug als Open-Source-Projekt weiterführte und es für Entwickler kostenlos wurde. Auch heute wird noch aktiv an Xamarin weiterentwickelt und es werden regelmäßig neue Updates veröffentlicht.

Erfahrungen aus der Cross-Plattform-Entwicklung mit Xamarin

Der erste Teil der Beitragsreihe hat die unterschiedlichen Ansätze der Cross-Plattform-Frameworks erklärt. Xamarin setzt auf eine geteilte (also plattformunabhängige) Applikationslogik und ein nativ entwickeltes (also plattformspezifisches) User Interface (UI). Es nutzt plattformabhängigen Code beispielsweise, um GPS- oder Beschleunigungssensoren anzusprechen. Manche APIs stellt Xamarin jedoch noch nicht plattformunabhängig bereit. Dieser Teil lässt sich in C# oder aber auch nativ, etwa in Java qoder Swift, entwickeln. Für iOS bedeutet das zum Beispiel, dass Entwickler in ihren UI-Storyboards .xib-Dateien und die bekannten Klassen, Controller und Widgets der Apple-Frameworks nutzen. Der Xamarin-Ansatz ist gut geeignet, wenn man ein komplexes, sich nativ verhaltendes UI und einen großen Anteil plattformunabhängiger Geschäftslogik hat. Abbildung 1 zeigt zusammenfassend den groben Aufbau einer solchen Xamarin-App:

Grobaufbau von Xamarin-Apps (Abb. 1)

Grobaufbau von Xamarin-Apps (Abb. 1)

Beim Verwenden von Xamarin benötigen Entwickler nach wie vor pro Zielplattform ein Team, das sich mit den nativen Frameworks auskennt, da sowohl das UI als auch Hardwarezugriffe nicht abstrahiert werden. Lediglich die Dienstschicht wird bei Xamarin komplett als eine Einheit entwickelt. Den Umstand versucht Xamarin.Forms zu entschärfen. Dabei (s. Abb. 2) baut man statt einem plattformspezifischen ein plattformübergreifendes UI. Xamarin.Forms setzt auf XAML und C# als Techniken zum Erstellen des UI. Das Framework selbst kümmert sich um die Übersetzung der jeweils verwendeten Steuerelemente in native Elemente und ein dementsprechendes Aussehen. Durch diesen Ansatz kann sich Xamarin.Forms nur als kleinster gemeinsamer Nenner zwischen den Plattformen verstehen.

Ursprünglich entwickelt, um recht einfache UIs in native UI-Komponenten zu übersetzen, ist das Framework mittlerweile sehr mächtig. Es lassen sich damit auch komplexe UIs umsetzen, da es zunehmend mehr native UI-Elemente unterstützt. Xamarin.Forms lässt sich zudem mit nativen UI-Elementen kombinieren, um spezielle plattformabhängige UIs zu bauen.

Die Abbildung des Xamarin.Forms UI auf native Elemente erfolgt durch besondere Klassen, sogenannte Renderer. Sie sind für Standardelemente bereits definiert. Man kann diese Klassen auch anpassen oder selbst Renderer für eigene UI-Elemente erstellen. Renderer sind für jede unterstützte Plattform zu entwickeln, da man das Mapping zwischen der jeweiligen nativen Komponente und dem Xamarin.Forms-Pendant definieren muss. Das native UI wird nur noch aus dem Xamarin.Forms UI und den Renderern generiert.

Dadurch hat Xamarin mit Xamarin.Forms einen Schritt weiter in Richtung plattformunabhängige Entwicklung vollzogen, ohne das native Look & Feel der Plattformen zu beseitigen. Und damit steigt das Potenzial, Code wiederverwenden zu können.

Grobaufbau von Xamarin.Forms (Abb. 2)

Grobaufbau von Xamarin.Forms (Abb. 2)

Dennoch sind nach wie vor Sensor-APIs (z.B. Kamera, Gyroskop und GPS) direkt nativ anzusprechen oder durch Plug-ins zu kapseln, die diese Aufgabe übernehmen. Die Plug-ins stammen häufig von Drittanbietern und sind auf spezielle Probleme zugeschnittene Insellösungen, die mit der Zeit zu Wildwuchs und Inkompatibilitäten zwischen den Plug-ins geführt haben. Das Problem versucht die Xamarin-Community nun mit Xamarin.Essentials zu entschärfen. Diese vereinigen zahlreiche häufig genutzte Systemfunktionen und APIs unter einem Dach. So ist sichergestellt, dass sich die Erweiterung, die die Aufnahme eines Fotos mit der Kamera ansteuert, mit der verträgt, die das Gyroskop bedient. Damit reduziert sich die Notwendigkeit, plattformspezifischen Code zu nutzen, noch weiter.

Im Folgenden erläutern die Autoren, wie man mit Xamarin.Forms eine App baut, und gehen dabei auf einige wesentliche Details der Implementierung ein.

Es ist wichtig, schon beim Entwurf der Architektur und des Designs plattformunabhängige Komponenten zu identifizieren und zu berücksichtigen. Eine Xamarin.Forms-App für iOS und Android besteht in der Regel aus drei Projekten in einer Visual Studio Solution (Projektmappe): MyApp.iOS und MyApp.Android für die plattformspezifischen Teile, die wiederum das geteilte Projekt MyApp.Shared referenzieren. Dabei ist es nichts weiter als eine Sammlung von Code ohne eigene Abhängigkeiten, der nur Gültigkeit in Kombination mit den Plattformprojekten in der Solution hat. Es ist auch möglich, ein .NET-Standard-2.0-Projekt für den geteilten Code zu nutzen, das eigenständig kompilierbar ist und eingebunden werden kann.

Unabhängig von der gewählten Variante besteht das Ziel darin, möglichst viel plattformunabhängigen Code in den geteilten Projekten zu haben und möglichst wenig plattformspezifischen. Es ist außerdem empfehlenswert, auf Conditional Compilation (Teile des Codes werden nur für eine bestimmte Plattform kompiliert) oder Plattformabfragen im geteilten Code zu verzichten und entsprechende Unterscheidungen beispielsweise durch den DependencyService oder andere Dependency-Injection-Frameworks in die Plattformprojekte auszulagern.

Der DependencyService [2] ermöglicht es, innerhalb des geteilten Codes auf plattformspezifische Implementierungen zuzugreifen. Dafür werden Funktionen über ein Interface beschrieben, in jedem Plattformprojekt implementiert und mit Annotation beim DependencyService registriert. Solch eine registrierte Dependency lässt sich dann über den DependencyService anfragen und verwenden.

Wichtig ist zu beachten, dass Xamarin nicht nur für Android und iOS ist, sondern auch Windows, macOS, Tizen, tvOS und weitere Plattformen bedient. Daher gibt es immer wieder Elemente, die nach wie vor spezifisch für jede Plattform zu bauen sind. Dinge wie Push-Nachrichten und Notifications oder Sensorzugriffe funktionieren auf jeder Plattform etwas anders und werden daher von Xamarin nicht allgemein gelöst. Zusätzlich sind die Ressourcen und Assets in den Plattformprojekten zu hinterlegen sowie unter iOS der Launch-Screen als Storyboard. Diese "doppelte" Verwaltung von Ressourcen ist nötig, da iOS- und Android-Geräte unterschiedliche Anforderungen an die Größe von Ressourcen haben. Will man auch einen Launch-Screen für Android, kann man ihn als eigene Activity im Android-Projekt bauen.

Mit Xamarin.Forms erstellte UIs bestehen aus einzelnen Seiten, zwischen denen auf verschiedene Weise navigiert wird. Es gibt zwei nicht exklusive Möglichkeiten, eine Seite (ViewController) in Xamarin.Forms zu bauen: zum einen rein per Code in C#, zum anderen mit XAML für das Layout und C# für die Logik. XAML ist eine XML-basierte Layout-Sprache, die beispielsweise auch bei Windows-UWP- oder WPF-Anwendungen (Windows Presentation Foundation) zum Einsatz kommt. Beide Mittel lassen sich beliebig miteinander kombinieren. Durch die Unterstützung von Bindings kann man einfach ViewModels nutzen, um eine MVVM-Architektur (Model View ViewModel) umzusetzen. Durch Bindings wird das UI automatisch aktualisiert, wenn Properties im ViewModel geändert werden. Hängt das Binding an einem interaktiv veränderbaren Attribut, gelangt der entsprechende Wert auch automatisch in das ViewModel (bidirektionale Bindings).

Im Folgenden ist ein kleines Beispiel zu sehen, das verschiedene UI-Elemente in XAML zeigt und wie diese schließlich auf Android und iOS aussehen (Abb. 3 und 4).

Listing 1: DemoPage.xaml

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="DemoProject.DemoPage"
             Title="DemoPage">
    <ContentPage.Content>
        <StackLayout Orientation="Vertical" Padding="16">
            <Label 
                Text="Standard Label"
                VerticalOptions="Start" HorizontalOptions="CenterAndExpand" ></Label>
            <ActivityIndicator 
                IsRunning="true" 
                WidthRequest="40" HeightRequest="40" 
                VerticalOptions="Start" HorizontalOptions="CenterAndExpand" ></ActivityIndicator>
            <DatePicker Date="{Binding Date}" ></DatePicker>
            <Switch x:Name ="MySwitch" IsToggled="{Binding IsSwitchToggled}" ></Switch>
        </StackLayout>
    </ContentPage.Content>
</ContentPage>
Listing 2: DemoPage.xaml.cs

using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;

namespace DemoProject
{
    [XamlCompilation(XamlCompilationOptions.Compile)]
    public partial class DemoPage : ContentPage
    {
        public DemoPage()
        {
            InitializeComponent();
            BindingContext = new DemoPageViewModel();
        }
    }

    public class DemoPageViewModel : INotifyPropertyChanged
    {
        private DateTime _date = DateTime.Now;
        public DateTime Date
        {
            get => _date;
            set
            {
                _date = value;
                OnPropertyChanged();
            }
        }

        private bool _isSwitchToggled;
        public bool IsSwitchToggled
        {
            get => _isSwitchToggled;
            set
            {
                _isSwitchToggled = value;
                OnPropertyChanged();
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}
Erfahrungen aus der Cross-Plattform-Entwicklung mit Xamarin – die Übersicht
Erfahrungen aus der Cross-Plattform-Entwicklung mit Xamarin – die Übersicht

Es wird deutlich, dass das gleiche Xamarin.Forms-Element auf Android und iOS leicht unterschiedlich aussieht, weil das native Look & Feel durch das Mapping auf die nativen Komponenten beibehalten wird. Dieses Mapping erreichen Entwickler durch Renderer.

Wie beschrieben und in Abbildung 2 zu sehen, basiert Xamarin.Forms auf Renderern, welche die plattformunabhängigen UI-Elemente in ihre nativen Gegenstücke umwandeln. Dabei werden im einfachsten Fall nur die Aufrufe auf Attribute und Funktionen auf die nativen APIs des jeweiligen Elements gemappt, während für kompliziertere Elemente noch viel Code zu schreiben ist, um beispielsweise die verschiedenen Events handhaben zu können.

Xamarin.Forms bringt schon Renderer für die Standardelemente mit, die zumindest den kleinsten gemeinsamen Nenner der dazugehörenden nativen Elemente abdecken. Will man neuere oder eine spezielle Funktionen eines nativen Elements auch auf Xamarin.Forms-Ebene nutzen können, muss man oft den Renderer erweitern, idealerweise beschränkt auf eine Subklasse des Xamarin.Forms-Elements, damit sich der Renderer nicht auf alle Instanzen dieses Typs auswirkt.

Ein Beispiel eines Renderers für ein angepasstes Element wäre ein Xamarin.Forms-Label, das unterstreichbar ist. Das Standard-Label kann man fett und/oder kursiv gestalten, allerdings fehlt die Möglichkeit, den Text zu unterstreichen. Deswegen haben die Autoren mit UnderlinableLabel ein eigenes Label erstellt, das unterstreichbar ist:

Listing 3: MyApp.Shared.UnderlinableLabel

using Xamarin.Forms;

namespace MyApp.Shared
{
    public class UnderlinableLabel : Label
    {
        public bool IsUnderlined { get; set; } = false;
    }
}

Dieses Label hat eine Property, die bestimmt, ob es unterstrichen ist oder nicht:

Listing 4: MyApp.iOS.Renderer.UnderlinableLabelRenderer

using Foundation;
using MyApp.iOS.Renderer;
using MyApp.Shared;
using UIKit;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;

[assembly: ExportRenderer(typeof(UnderlinableLabel), typeof(UnderlinableLabelRenderer))]
namespace MyApp.iOS.Renderer
{
    public class UnderlinableLabelRenderer : LabelRenderer
    {
        protected override void OnElementChanged(ElementChangedEventArgs<Label> e)
        {
            base.OnElementChanged(e);
            if (Element is UnderlinableLabel view)
            {
                UpdateUnderline(view, Control);
            }
        }

        private static void UpdateUnderline(UnderlinableLabel view, UILabel control)
        {
            if (view.IsUnderlined)
            {
                var attributedString = new NSMutableAttributedString(view.Text);
                attributedString.AddAttribute(
                    UIStringAttributeKey.UnderlineStyle,
                    NSNumber.FromInt32((int)NSUnderlineStyle.Single),
                    new NSRange(0, attributedString.Length));
                control.AttributedText = attributedString;
            }
            else
            {
                control.AttributedText = new NSMutableAttributedString(view.Text);
            }
        }
    }
}

Und dies ist der Android-Renderer:

Listing 5: MyApp.Droid.Renderer.UnderlinableLabelRenderer

using Android.Content;
using Android.Graphics;
using Android.Widget;
using MyApp.Droid.Renderer;
using MyApp.Shared;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;

[assembly: ExportRenderer(typeof(UnderlinableLabel), typeof(UnderlinableLabelRenderer))]
namespace MyApp.Droid.Renderer
{
    public class UnderlinableLabelRenderer : LabelRenderer
    {
        public UnderlinableLabelRenderer(Context context) : base(context) { }

        protected override void OnElementChanged(ElementChangedEventArgs<Label> e)
        {
            base.OnElementChanged(e);
            if (Element is UnderlinableLabel view)
            {
                UpdateUnderline(view, Control);
            }
        }

        private static void UpdateUnderline(UnderlinableLabel view, TextView control)
        {
            if (view.IsUnderlined)
                control.PaintFlags = control.PaintFlags | PaintFlags.UnderlineText;
            else
                control.PaintFlags = control.PaintFlags & ~PaintFlags.UnderlineText;
        }
    }
}

Da es keinen speziellen Renderer für den Typ UnderlinableLabel gibt, wird der nächstpassende Renderer, nämlich der vom Supertyp Label, genommen. Er nutzt aber nicht die definierte Property IsUnderlined. Deswegen ist jeweils für jede unterstützte Plattform ein Renderer zu schreiben, der aufgrund dieses Werts mit der nativen APIs das native UI-Element entweder unterstreicht oder nicht.

Im Beispiel ist der native Code zum Unterstreichen markiert; der Rest des Codes ist nur für den Renderer nötig. Dieser Teil ist durch Xamarins API-Mapping recht ähnlich zur nativen Variante, denn auch nativ muss man Unterstreichen etwas umständlicher setzen. Mittlerweile unterstützt Xamarin.Forms über Text Decorations auch Underline und Strikethrough [3], allerdings nur für das Label.

Es lassen sich allerdings selbst Renderer schreiben, die komplett eigene Elemente in das jeweilige native Pendant umsetzen. Die Elemente erben dann im schlechtesten Fall nur von View und die Renderer folglich auch nur von ViewRenderer. Man muss sodann den kompletten Lifecycle und die Properties des Elements modellieren, und das jeweils für jede Zielplattform.

In diesem Artikel haben wir uns Xamarin.Forms genauer angeschaut und wie man damit plattformübergreifende UIs bauen kann. Xamarin.Forms erlaubt mit einfachen Mitteln eine App auf unterschiedlichen Plattformen bereitzustellen und trotzdem die Charakteristiken, der jeweiligen Zielplattform widerzuspiegeln. An den Stellen, wo Xamarin Einschränkungen aufweist können Entwickler Erweiterungen installieren oder Xamarin in Form von eigenen Renderern selbst erweitern.

Im nächsten Beitrag zum Thema geht es um DevOps im Kontext von Xamarin.Forms und hier insbesondere um Continuous Delivery, automatisierte Unit- und UI-Tests sowie Teamstruktur und -organisation. Des Weiteren werden die Autoren einen Ausblick auf die Zukunft von Xamarin geben, denn mit Xamarin.Essentials ist in den letzten Monaten eine weitere Bibliothek hinzugekommen, die noch mehr geteilten Code ermöglicht, was einige der bisherigen Limitierungen von Xamarin und Xamarin.Forms entschärft.

Simon André Scherr
ist seit über sechs Jahren im Bereich mobile Software Engineering mit Fokus auf User Experience unterwegs. Neben der Organisation von Entwicklungsprojekten im mobilen Bereich ist sein derzeitiger Schwerpunkt die Einbezugnahme des Nutzers in die schnelllebige Produktentwicklung.

Steffen Hupp
arbeitet seit 2015 als Mobile Software Engineer im Fraunhofer IESE in Kaiserslautern. Im Projekt Digitale Dörfer liegen seine Aufgaben in der technischen Konzeption und Umsetzung im Kontext der Cross-Plattform-Entwicklung sowie die Automatisierung von Vorgängen in der Entwicklung.

Patrick Mennig
ist Wirtschaftsinformatiker mit dem Schwerpunkt Anforderungserhebung und Innovation für Informationssysteme. Sein Schwerpunkt liegt auf Kreativworkshops und -techniken für softwarebasierte Innovationen. Dafür entwickelt er immer wieder Prototypen und Anwendungen mit Web-Technologien, insbesondere React. Seit 2017 arbeitet er am Fraunhofer IESE in verschiedenen Forschungs- und Innovationsprojekten. (ane [4])


URL dieses Artikels:
https://www.heise.de/-4684223

Links in diesem Artikel:
[1] https://www.heise.de/hintergrund/Erfahrungen-aus-der-Cross-Plattform-Entwicklung-mit-Xamarin-Teil-1-Apps-und-Frameworks-4452648.html
[2] https://docs.microsoft.com/en-us/xamarin/xamarin-forms/app-fundamentals/dependency-service/introduction
[3] https://docs.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/text/label#text-decorations
[4] mailto:ane@heise.de