Entwurfsprinzipien der Xamarin.Android-API

Zusätzlich zu den kernigen Basisklassenbibliotheken, die Teil von Mono sind, wird Xamarin.Android mit Bindungen für verschiedene Android-APIs ausgeliefert, damit Entwickler native Android-Anwendungen mit Mono erstellen können.

Im Kern von Xamarin.Android gibt es eine Interop-Engine, die die C#-Welt mit der Java-Welt verbindet und Entwicklern Zugriff auf die Java-APIs aus C# oder anderen .NET-Sprachen bietet.

Entwurfsprinzipien

Dies sind einige unserer Designprinzipien für die Xamarin.Android-Bindung

  • Entspricht den .NET Framework Entwurfsrichtlinien.

  • Zulassen, dass Entwickler Java-Klassen unterklassen.

  • Die Unterklasse sollte mit C#-Standardkonstrukten funktionieren.

  • Von einer vorhandenen Klasse ableiten.

  • Rufen Sie den Basiskonstruktor auf, um die Kette zu verketten.

  • Das Überschreiben von Methoden sollte mit dem Überschreibungssystem von C# erfolgen.

  • Machen Sie gängige Java-Aufgaben einfach und harte Java-Aufgaben möglich.

  • Machen Sie JavaBean-Eigenschaften als C#-Eigenschaften verfügbar.

  • Machen Sie eine stark typisierte API verfügbar:

    • Erhöhen sie die Typsicherheit.

    • Minimieren Sie Laufzeitfehler.

    • Rufen Sie IDE intellisense für Rückgabetypen ab.

    • Ermöglicht die Dokumentation zu IDE-Popups.

  • Fördern Sie in der IDE die Erkundung der APIs:

    • Verwenden Sie Framework-Alternativen, um die Verfügbarkeit von Java Classlib zu minimieren.

    • Machen Sie C#-Delegaten (Lambdas, anonyme Methoden und System.Delegate) anstelle von Schnittstellen mit nur einer Methode verfügbar, wenn dies angemessen und zutreffend ist.

    • Stellen Sie einen Mechanismus zum Aufrufen beliebiger Java-Bibliotheken bereit ( Android.Runtime.JNIEnv).

Assemblys

Xamarin.Android enthält eine Reihe von Assemblys, die das MonoMobile-Profil bilden. Weitere Informationen finden Sie auf der Seite Assemblys.

Die Bindungen an die Android-Plattform sind in der Mono.Android.dll Assembly enthalten. Diese Assembly enthält die gesamte Bindung für die Nutzung von Android-APIs und die Kommunikation mit der Android-Runtime-VM.

Bindungsdesign

Sammlungen

Die Android-APIs nutzen die java.util-Sammlungen umfassend, um Listen, Sätze und Zuordnungen bereitzustellen. Wir machen diese Elemente mithilfe der System.Collections.Generic-Schnittstellen in unserer Bindung verfügbar. Die grundlegenden Zuordnungen sind:

Wir haben Hilfsklassen bereitgestellt, um ein schnelleres kopierloses Marshallen dieser Typen zu ermöglichen. Wenn möglich, wird empfohlen, diese bereitgestellten Sammlungen anstelle der vom Framework bereitgestellten Implementierung wie List<T> oder Dictionary<TKey, TValue>zu verwenden. Die Android.Runtime-Implementierungen verwenden intern eine native Java-Auflistung und erfordern daher kein Kopieren in und aus einer nativen Sammlung, wenn sie an ein Android-API-Mitglied übergeben wird.

Sie können jede Schnittstellenimplementierung an eine Android-Methode übergeben, die diese Schnittstelle akzeptiert, z. B. an List<int> den ArrayAdapter<int>(Context, int, IList<int>)- Konstruktor übergeben. Für alle Implementierungen mit Ausnahme der Android.Runtime-Implementierungen muss die Liste jedoch von der Mono-VM in die Android-Runtime-VM kopiert werden. Wenn die Liste später innerhalb der Android-Runtime geändert wird (z. B. durch Aufrufen des ArrayAdapter<T>. Add(T) -Methode), sind diese Änderungen im verwalteten Code nicht sichtbar. Wenn ein JavaList<int> verwendet würde, wären diese Änderungen sichtbar.

Neu formulierte Implementierungen der Sammlungsschnittstelle, die nicht zu den oben aufgeführten Hilfsklassengehören, marshallen nur [In]:

// This fails:
var badSource  = new List<int> { 1, 2, 3 };
var badAdapter = new ArrayAdapter<int>(context, textViewResourceId, badSource);
badAdapter.Add (4);
if (badSource.Count != 4) // true
    throw new InvalidOperationException ("this is thrown");

// this works:
var goodSource  = new JavaList<int> { 1, 2, 3 };
var goodAdapter = new ArrayAdapter<int> (context, textViewResourceId, goodSource);
goodAdapter.Add (4);
if (goodSource.Count != 4) // false
    throw new InvalidOperationException ("should not be reached.");

Eigenschaften

Java-Methoden werden ggf. in Eigenschaften transformiert:

  • Das Java-Methodenpaar T getFoo() und void setFoo(T) werden in die Foo -Eigenschaft transformiert. Beispiel: Activity.Intent.

  • Die Java-Methode getFoo() wird in die schreibgeschützte Foo-Eigenschaft transformiert. Beispiel: Context.PackageName.

  • Set-only-Eigenschaften werden nicht generiert.

  • Eigenschaften werden nicht generiert, wenn der Eigenschaftentyp ein Array ist.

Ereignisse und Listener

Die Android-APIs basieren auf Java, und ihre Komponenten folgen dem Java-Muster zum Einbinden von Ereignislistenern. Dieses Muster ist in der Regel umständlich, da der Benutzer eine anonyme Klasse erstellen und die Methoden deklarieren muss, um überschrieben zu werden, z. B. in Android mit Java:

final android.widget.Button button = new android.widget.Button(context);

button.setText(this.count + " clicks!");
button.setOnClickListener (new View.OnClickListener() {
    public void onClick (View v) {
        button.setText(++this.count + " clicks!");
    }
});

Der entsprechende Code in C# mit Ereignissen wäre:

var button = new Android.Widget.Button (context) {
    Text = string.Format ("{0} clicks!", this.count),
};
button.Click += (sender, e) => {
    button.Text = string.Format ("{0} clicks!", ++this.count);
};

Beachten Sie, dass beide der oben genannten Mechanismen mit Xamarin.Android verfügbar sind. Sie können eine Listenerschnittstelle implementieren und mit View.SetOnClickListener anfügen, oder Sie können einen Delegaten, der über eines der üblichen C#-Paradigmen erstellt wurde, an das Click-Ereignis anfügen.

Wenn die Listener-Rückrufmethode eine void-Rückgabe aufweist, erstellen wir API-Elemente basierend auf einem EventHandler<TEventArgs-Delegaten> . Wir generieren ein Ereignis wie das obige Beispiel für diese Listenertypen. Wenn der Listener-Rückruf jedoch einen nicht leeren und nicht booleschen Wert zurückgibt, werden Ereignisse und EventHandler nicht verwendet. Stattdessen generieren wir einen bestimmten Delegaten für die Signatur des Rückrufs und fügen anstelle von Ereignissen Eigenschaften hinzu. Der Grund besteht darin, sich mit der Reihenfolge des Delegataufrufs und der Rückgabebehandlung zu befassen. Dieser Ansatz spiegelt die Vorgänge mit der Xamarin.iOS-API wieder.

C#-Ereignisse oder -Eigenschaften werden nur automatisch generiert, wenn die Android-Ereignisregistrierungsmethode:

  1. Hat ein set Präfix, z. B. Festlegenvon OnClickListener.

  2. Verfügt über einen void Rückgabetyp.

  3. Akzeptiert nur einen Parameter, der Parametertyp ist eine Schnittstelle, die Schnittstelle verfügt nur über eine Methode, und der Schnittstellenname endet auf Listener , z. B. View.OnClick Listener.

Darüber hinaus enthält die generierte EventArgs-Unterklasse eine Handled-Eigenschaft, wenn die Listener-Schnittstellenmethode einen Rückgabetyp von boolean anstelle von void aufweist. Der Wert der Handled-Eigenschaft wird als Rückgabewert für die Listener-Methode verwendet und ist standardmäßig auf true.

Beispielsweise akzeptiert die Android View.setOnKeyListener()- Methode die View.OnKeyListener-Schnittstelle , und die View.OnKeyListener.onKey(View, int, KeyEvent) -Methode verfügt über einen booleschen Rückgabetyp. Xamarin.Android generiert ein entsprechendes View.KeyPress-Ereignis , bei dem es sich um ein EventHandler<View.KeyEventArgs> handelt. Die KeyEventArgs-Klasse verfügt wiederum über eine View.KeyEventArgs.Handled-Eigenschaft , die als Rückgabewert für die View.OnKeyListener.onKey()- Methode verwendet wird.

Wir beabsichtigen, Überladungen für andere Methoden und Ctors hinzuzufügen, um die delegatenbasierte Verbindung verfügbar zu machen. Außerdem erfordern Listener mit mehreren Rückrufen eine zusätzliche Überprüfung, um festzustellen, ob die Implementierung einzelner Rückrufe sinnvoll ist. Daher konvertieren wir diese, sobald sie identifiziert werden. Wenn es kein entsprechendes Ereignis gibt, müssen Listener in C# verwendet werden, aber bitte bringen Sie uns alle zur Kenntnis, von denen Sie glauben, dass die Verwendung delegiert werden könnte. Wir haben auch einige Konvertierungen von Schnittstellen ohne das Suffix "Listener" durchgeführt, als klar war, dass sie von einer Delegatenalternative profitieren würden.

Alle Listenerschnittstellen implementieren dieAndroid.Runtime.IJavaObject aufgrund der Implementierungsdetails der Bindung, sodass Listenerklassen diese Schnittstelle implementieren müssen. Dies kann durch Implementieren der Listenerschnittstelle in einer Unterklasse von Java.Lang.Object oder einem anderen umschlossenen Java-Objekt, z. B. einer Android-Aktivität, erfolgen.

Runnables

Java verwendet die java.lang.Runnable-Schnittstelle , um einen Delegierungsmechanismus bereitzustellen. Die java.lang.Thread-Klasse ist ein bemerkenswerter Consumer dieser Schnittstelle. Android hat die Schnittstelle auch in der API verwendet. Activity.runOnUiThread() und View.post() sind bemerkenswerte Beispiele.

Die Runnable Schnittstelle enthält eine einzelne void-Methode, run(). Es eignet sich daher für die Bindung in C# als System.Action-Delegat . Wir haben Überladungen in der Bindung bereitgestellt, die einen Action Parameter für alle API-Member akzeptieren, die einen Runnable in der nativen API verwenden, z. B. Activity.RunOnUiThread() und View.Post().

Wir haben die IRunnable-Überladungen an Ort und Stelle gelassen, anstatt sie zu ersetzen, da mehrere Typen die Schnittstelle implementieren und daher direkt als runnables übergeben werden können.

Innere Klassen

Java verfügt über zwei verschiedene Arten geschachtelter Klassen: statische geschachtelte Klassen und nicht statische Klassen.

Statische geschachtelte Java-Klassen sind mit geschachtelten C#-Typen identisch.

Nicht statische geschachtelte Klassen, auch innere Klassen genannt, unterscheiden sich erheblich. Sie enthalten einen impliziten Verweis auf eine instance ihres eingeschlossenen Typs und dürfen keine statischen Elemente enthalten (neben anderen Unterschieden außerhalb des Geltungsbereichs dieser Übersicht).

Wenn es um Bindung und C#-Verwendung geht, werden statische geschachtelte Klassen als normale geschachtelte Typen behandelt. Innere Klassen weisen zwei erhebliche Unterschiede auf:

  1. Der implizite Verweis auf den enthaltenden Typ muss explizit als Konstruktorparameter angegeben werden.

  2. Beim Erben von einer inneren Klasse muss die innere Klasse in einen Typ geschachtelt werden, der vom enthaltenden Typ der inneren Basisklasse erbt, und der abgeleitete Typ muss einen Konstruktor desselben Typs wie der enthaltende C#-Typ bereitstellen.

Betrachten Sie beispielsweise die innere Android.Service.WallpaperService.Engine-Klasse . Da es sich um eine innere Klasse handelt, nimmt der WallpaperService.Engine()-Konstruktor einen Verweis auf eine WallpaperService-instance (Vergleich und Kontrast zum Java WallpaperService.Engine()-Konstruktor, der keine Parameter akzeptiert).

Eine Beispielableitung einer inneren Klasse ist CubeWallpaper.CubeEngine:

class CubeWallpaper : WallpaperService {
    public override WallpaperService.Engine OnCreateEngine ()
    {
        return new CubeEngine (this);
    }

    class CubeEngine : WallpaperService.Engine {
        public CubeEngine (CubeWallpaper s)
                : base (s)
        {
        }
    }
}

Beachten Sie, wie CubeWallpaper.CubeEngine in CubeWallpapergeschachtelt wird, CubeWallpaper erbt von der enthaltenden Klasse von WallpaperService.Engineund CubeWallpaper.CubeEngine verfügt über einen Konstruktor, der den deklarierenden Typ - CubeWallpaper in diesem Fall - alle wie oben angegeben übernimmt.

Schnittstellen

Java-Schnittstellen können drei Sätze von Membern enthalten, von denen zwei Probleme in C# verursachen:

  1. Methoden

  2. Typen

  3. Felder

Java-Schnittstellen werden in zwei Typen übersetzt:

  1. Eine (optionale) Schnittstelle, die Methodendeklarationen enthält. Diese Schnittstelle hat den gleichen Namen wie die Java-Schnittstelle, aber sie hat auch das Präfix " I ".

  2. Eine (optionale) statische Klasse, die alle innerhalb der Java-Schnittstelle deklarierten Felder enthält.

Geschachtelte Typen werden "verschoben", um gleichgeordnete Typen der schließenden Schnittstelle anstelle von geschachtelten Typen zu sein, wobei der einschließende Schnittstellenname als Präfix gilt.

Betrachten Sie beispielsweise die Android.os.Parcelable-Schnittstelle . Die Parcelable-Schnittstelle enthält Methoden, geschachtelte Typen und Konstanten. Die Parcelable-Schnittstellenmethoden werden in der Android.OS.IParcelable-Schnittstelle platziert. Die Paketable-Schnittstellenkonstanten werden im Android.OS.ParcelableConsts-Typ platziert. Die geschachtelten Typen android.os.Parcelable.ClassLoaderCreator<T> und android.os.Parcelable.Creator<T> sind derzeit aufgrund von Einschränkungen in unserer Generics-Unterstützung nicht gebunden. Wenn sie unterstützt würden, wären sie als Die Schnittstellen Android.OS.IParcelableClassLoaderCreator und Android.OS.IParcelableCreator vorhanden. Beispielsweise ist die geschachtelte android.os.IBinder.DeathRecipient-Schnittstelle als Android.OS.IBinderDeathRecipient-Schnittstelle gebunden.

Hinweis

Ab Xamarin.Android 1.9 werden Java-Schnittstellenkonstanten dupliziert , um das Portieren von Java-Code zu vereinfachen. Dies hilft, die Portierung von Java-Code zu verbessern, der auf Android-Anbieterschnittstellenkonstanten basiert.

Zusätzlich zu den oben genannten Typen gibt es vier weitere Änderungen:

  1. Ein Typ mit demselben Namen wie die Java-Schnittstelle wird generiert, um Konstanten zu enthalten.

  2. Typen, die Schnittstellenkonstanten enthalten, enthalten auch alle Konstanten, die von implementierten Java-Schnittstellen stammen.

  3. Alle Klassen, die eine Java-Schnittstelle mit Konstanten implementieren, erhalten einen neuen geschachtelten InterfaceConsts-Typ, der Konstanten von allen implementierten Schnittstellen enthält.

  4. Der Consts-Typ ist jetzt veraltet.

Für die android.os.Parcelable-Schnittstelle bedeutet dies, dass es jetzt einen Android.OS.Parcelable-Typ gibt, der die Konstanten enthält. Die Parcelable.CONTENTS_FILE_DESCRIPTOR-Konstante wird beispielsweise als Parcelable.ContentsFileDescriptor-Konstante und nicht als ParcelableConsts.ContentsFileDescriptor-Konstante gebunden.

Für Schnittstellen, die Konstanten enthalten, die andere Schnittstellen implementieren, die noch mehr Konstanten enthalten, wird nun die Union aller Konstanten generiert. Beispielsweise implementiert die Android.provider.MediaStore.VideoColumns-Schnittstelle die Android.provider.MediaStore.MediaColumns-Schnittstelle . Vor Version 1.9 hat der Android.Provider.MediaStore.Video.VideoColumnsConsts-Typ jedoch keine Möglichkeit, auf die unter Android.Provider.MediaStore.MediaColumnsConsts deklarierten Konstanten zuzugreifen. Daher muss der Java-Ausdruck MediaStore.Video.VideoColumns.TITLE an den C#-Ausdruck MediaStore.Video.MediaColumnsConsts.Title gebunden werden, der nur schwer zu ermitteln ist, ohne viel Java-Dokumentation zu lesen. In Version 1.9 lautet der entsprechende C#-Ausdruck MediaStore.Video.VideoColumns.Title.

Betrachten Sie außerdem den Android.os.Bundle-Typ , der die Java Parcelable-Schnittstelle implementiert. Da die Schnittstelle implementiert wird, sind alle Konstanten auf dieser Schnittstelle "über" den Bundle-Typ zugänglich, z. B. Bundle.CONTENTS_FILE_DESCRIPTOR ein vollkommen gültiger Java-Ausdruck ist. Zuvor mussten Sie zum Portieren dieses Ausdrucks in C# alle Implementierten Schnittstellen betrachten, um zu sehen, von welchem Typ der CONTENTS_FILE_DESCRIPTOR stammt. Ab Xamarin.Android 1.9 verfügen Klassen, die Java-Schnittstellen implementieren, die Konstanten enthalten, über einen geschachtelten InterfaceConsts-Typ , der alle geerbten Schnittstellenkonstanten enthält. Dadurch kann Bundle.CONTENTS_FILE_DESCRIPTOR in Bundle.InterfaceConsts.ContentsFileDescriptor übersetzt werden.

Schließlich sind Typen mit einem Consts-Suffix wie Android.OS.ParcelableConsts jetzt veraltet, anders als die neu eingeführten geschachtelten InterfaceConsts-Typen. Sie werden in Xamarin.Android 3.0 entfernt.

Ressourcen

Bilder, Layoutbeschreibungen, Binäre Blobs und Zeichenfolgenwörterbücher können als Ressourcendateien in Ihre Anwendung aufgenommen werden. Verschiedene Android-APIs sind für den Betrieb auf den Ressourcen-IDs konzipiert, anstatt sich direkt mit Bildern, Zeichenfolgen oder binären Blobs zu befassen.

Beispielsweise würde eine Android-Beispiel-App, die ein Benutzeroberflächenlayout ( main.axml), eine Internationalisierungstabellenzeichenfolge ( strings.xml) und einige Symbole ( drawable-*/icon.png) enthält, ihre Ressourcen im Verzeichnis "Ressourcen" der Anwendung beibehalten:

Resources/
    drawable-hdpi/
        icon.png

    drawable-ldpi/
        icon.png

    drawable-mdpi/
        icon.png

    layout/
        main.axml

    values/
        strings.xml

Die nativen Android-APIs arbeiten nicht direkt mit Dateinamen, sondern mit Ressourcen-IDs. Wenn Sie eine Android-Anwendung kompilieren, die Ressourcen verwendet, packt das Buildsystem die Ressourcen für die Verteilung und generiert eine Klasse namens Resource , die die Token für jede der enthaltenen Ressourcen enthält. Für das obige Ressourcenlayout würde die R-Klasse beispielsweise Folgendes verfügbar machen:

public class Resource {
    public class Drawable {
        public const int icon = 0x123;
    }

    public class Layout {
        public const int main = 0x456;
    }

    public class String {
        public const int first_string = 0xabc;
        public const int second_string = 0xbcd;
    }
}

Sie würden dann verwendenResource.Drawable.icon, um auf die drawable/icon.png Datei zu verweisen, Resource.Layout.main oder Resource.String.first_string um auf die layout/main.xml erste Zeichenfolge in der Wörterbuchdatei values/strings.xmlzu verweisen.

Konstanten und Enumerationen

Die nativen Android-APIs verfügen über viele Methoden, die einen int übernehmen oder zurückgeben, der einem konstanten Feld zugeordnet werden muss, um zu bestimmen, was int bedeutet. Um diese Methoden zu verwenden, muss der Benutzer die Dokumentation lesen, um zu sehen, welche Konstanten geeignete Werte sind, was weniger als ideal ist.

Beispiel : Activity.requestWindowFeature(int featureID).

In diesen Fällen versuchen wir, verwandte Konstanten in einer .NET-Enumeration zu gruppieren und die Methode neu zuzuordnen, um stattdessen die Enumeration zu übernehmen. Auf diese Weise können wir IntelliSense eine Auswahl der potenziellen Werte anbieten.

Das obige Beispiel lautet : Activity.RequestWindowFeature(WindowFeatures featureId).

Beachten Sie, dass dies ein sehr manueller Prozess ist, um herauszufinden, welche Konstanten zusammen gehören und welche APIs diese Konstanten nutzen. Bitte dateiieren Sie Fehler für alle Konstanten, die in der API verwendet werden und besser als Enumeration ausgedrückt werden.