Principes de conception de l’API Xamarin.Android

En plus des principales bibliothèques de classes de base qui font partie de Mono, Xamarin.Android est fourni avec des liaisons pour différentes API Android afin de permettre aux développeurs de créer des applications Android natives avec Mono.

Au cœur de Xamarin.Android se trouve un moteur d’interopérabilité qui relie le monde C# au monde Java et fournit aux développeurs un accès aux API Java à partir de C# ou d’autres langages .NET.

Principes de conception

Voici quelques-uns de nos principes de conception pour la liaison Xamarin.Android

  • Conformez-vous aux instructions de conception du .NET Framework.

  • Autoriser les développeurs à sous-classer les classes Java.

  • La sous-classe doit fonctionner avec des constructions standard C#.

  • Dérivez d’une classe existante.

  • Appelez le constructeur de base à la chaîne.

  • Le remplacement des méthodes doit être effectué avec le système de remplacement de C#.

  • Facilitez les tâches Java courantes et les tâches Java dures possibles.

  • Exposez les propriétés JavaBean en tant que propriétés C#.

  • Exposez une API fortement typée :

    • Augmentez la sécurité du type.

    • Réduisez les erreurs d’exécution.

    • Obtenez intellisense DE l’IDE sur les types de retour.

    • Autorise la documentation contextuelle de l’IDE.

  • Encouragez l’exploration dans l’IDE des API :

    • Utilisez des alternatives framework pour réduire l’exposition de Java Classlib.

    • Exposez des délégués C# (lambdas, méthodes anonymes et System.Delegate) au lieu d’interfaces à méthode unique lorsque cela est approprié et applicable.

    • Fournissez un mécanisme pour appeler des bibliothèques Java arbitraires ( Android.Runtime.JNIEnv).

Assemblys

Xamarin.Android inclut un certain nombre d’assemblys qui constituent le profil MonoMobile. La page Assemblys contient plus d’informations.

Les liaisons à la plateforme Android sont contenues dans l’assembly Mono.Android.dll . Cet assembly contient la liaison entière pour l’utilisation des API Android et la communication avec la machine virtuelle du runtime Android.

Conception de liaison

Collections

Les API Android utilisent largement les collections java.util pour fournir des listes, des jeux et des cartes. Nous exposons ces éléments à l’aide des interfaces System.Collections.Generic dans notre liaison. Les mappages fondamentaux sont les suivants :

Nous avons fourni des classes d’assistance pour faciliter le marshaling sans copie plus rapide de ces types. Lorsque cela est possible, nous vous recommandons d’utiliser ces collections fournies au lieu de l’implémentation fournie par l’infrastructure, comme List<T> ou Dictionary<TKey, TValue>. Les implémentations Android.Runtime utilisent une collection Java native en interne et ne nécessitent donc pas de copie vers et à partir d’une collection native lors du passage à un membre de l’API Android.

Vous pouvez passer n’importe quelle implémentation d’interface à une méthode Android acceptant cette interface, par exemple, passer un List<int> au constructeur ArrayAdapter<int>(Context, int, IList<int>). Toutefois, pour toutes les implémentations à l’exception des implémentations Android.Runtime, cela implique de copier la liste de la machine virtuelle Mono dans la machine virtuelle du runtime Android. Si la liste est modifiée ultérieurement dans le runtime Android (par exemple, en appelant ArrayAdapter<T>. Méthode Add(T ), ces modifications ne seront pas visibles dans le code managé. Si un JavaList<int> était utilisé, ces modifications seraient visibles.

Implémentations d’interface de collections reformulées qui ne font pas partie des classes d’assistancerépertoriées ci-dessus marshalent uniquement [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.");

Propriétés

Les méthodes Java sont transformées en propriétés, le cas échéant :

  • La paire T getFoo() de méthodes Java et void setFoo(T) sont transformées en propriété Foo . Exemple : Activity.Intent.

  • La méthode getFoo() Java est transformée en propriété Foo en lecture seule. Exemple : Context.PackageName.

  • Les propriétés set-only ne sont pas générées.

  • Les propriétés ne sont pas générées si le type de propriété est un tableau.

Événements et écouteurs

Les API Android sont basées sur Java et ses composants suivent le modèle Java pour raccorder des écouteurs d’événements. Ce modèle a tendance à être fastidieux, car il oblige l’utilisateur à créer une classe anonyme et à déclarer les méthodes à remplacer. Par exemple, voici comment procéder dans Android avec 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!");
    }
});

Le code équivalent en C# utilisant des événements serait :

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);
};

Notez que les deux mécanismes ci-dessus sont disponibles avec Xamarin.Android. Vous pouvez implémenter une interface d’écouteur et l’attacher à View.SetOnClickListener, ou vous pouvez attacher un délégué créé via l’un des paradigmes C# habituels à l’événement Click.

Lorsque la méthode de rappel de l’écouteur a un retour void, nous créons des éléments d’API basés sur un délégué TEventArgs> EventHandler<. Nous générons un événement comme l’exemple ci-dessus pour ces types d’écouteurs. Toutefois, si le rappel de l’écouteur retourne une valeur non void et non booléenne , les événements et les gestionnaires d’événements ne sont pas utilisés. Au lieu de cela, nous générons un délégué spécifique pour la signature du rappel et ajoutons des propriétés au lieu d’événements. La raison est de gérer l’ordre d’appel délégué et la gestion des retours. Cette approche reflète ce qui est fait avec l’API Xamarin.iOS.

Les événements ou propriétés C# ne sont générés automatiquement que si la méthode d’inscription aux événements Android :

  1. A un set préfixe, par exemple , définirOnClickListener.

  2. A un void type de retour.

  3. Accepte un seul paramètre, le type de paramètre est une interface, l’interface n’a qu’une seule méthode et le nom de l’interface se termine par Listener , par exemple View.OnClick Listener.

En outre, si la méthode d’interface Listener a un type de retour booléen au lieu de void, la sous-classe EventArgs générée contient une propriété Handled . La valeur de la propriété Handled est utilisée comme valeur de retour pour la méthode Listener , et elle est truedéfinie par défaut sur .

Par exemple, la méthode Android View.setOnKeyListener() accepte l’interface View.OnKeyListener , et la méthode View.OnKeyListener.onKey(View, int, KeyEvent) a un type de retour booléen. Xamarin.Android génère un événement View.KeyPress correspondant, qui est un EventHandler<View.KeyEventArgs>. La classe KeyEventArgs possède à son tour une propriété View.KeyEventArgs.Handled, qui est utilisée comme valeur de retour pour la méthode View.OnKeyListener.onKey().

Nous avons l’intention d’ajouter des surcharges pour d’autres méthodes et ctors afin d’exposer la connexion basée sur les délégués. En outre, les écouteurs avec plusieurs rappels nécessitent une inspection supplémentaire pour déterminer si l’implémentation de rappels individuels est raisonnable. Nous les convertissons donc à mesure qu’ils sont identifiés. S’il n’y a pas d’événement correspondant, les écouteurs doivent être utilisés en C#, mais veuillez porter à notre attention ceux qui, selon vous, pourraient avoir délégué l’utilisation. Nous avons également effectué certaines conversions d’interfaces sans le suffixe « Écouteur » alors qu’il était clair qu’elles bénéficieraient d’une alternative de délégué.

Toutes les interfaces d’écouteurs implémentent leAndroid.Runtime.IJavaObject en raison des détails d’implémentation de la liaison, les classes d’écouteur doivent donc implémenter cette interface. Pour ce faire, implémentez l’interface de l’écouteur sur une sous-classe de Java.Lang.Object ou tout autre objet Java encapsulé, tel qu’une activité Android.

Exécutables

Java utilise l’interface java.lang.Runnable pour fournir un mécanisme de délégation. La classe java.lang.Thread est un consommateur notable de cette interface. Android a également utilisé l’interface dans l’API. Activity.runOnUiThread() et View.post() sont des exemples notables.

L’interface Runnable contient une méthode void unique, run(). Il se prête donc à la liaison en C# en tant que délégué System.Action . Nous avons fourni des surcharges dans la liaison qui acceptent un Action paramètre pour tous les membres d’API qui consomment un Runnable dans l’API native, par exemple Activity.RunOnUiThread() et View.Post().

Nous avons laissé les surcharges IRunnable en place au lieu de les remplacer, car plusieurs types implémentent l’interface et peuvent donc être passés directement en tant que runnables.

Classes internes

Java a deux types différents de classes imbriquées : les classes imbriquées statiques et les classes non statiques.

Les classes imbriquées statiques Java sont identiques aux types imbriqués C#.

Les classes imbriquées non statiques, également appelées classes internes, sont considérablement différentes. Ils contiennent une référence implicite à un instance de leur type englobant et ne peuvent pas contenir de membres statiques (entre autres différences en dehors de l’étendue de cette vue d’ensemble).

En ce qui concerne la liaison et l’utilisation de C#, les classes imbriquées statiques sont traitées comme des types imbriqués normaux. Les classes internes, quant à elles, présentent deux différences significatives :

  1. La référence implicite au type conteneur doit être fournie explicitement en tant que paramètre de constructeur.

  2. Lors de l’héritage d’une classe interne, la classe interne doit être imbriquée dans un type qui hérite du type conteneur de la classe interne de base, et le type dérivé doit fournir un constructeur du même type que le type contenant C#.

Par exemple, considérez la classe interne Android.Service.Wallpaper.WallpaperService.Engine . Étant donné qu’il s’agit d’une classe interne, le constructeur WallpaperService.Engine() prend une référence à un instance WallpaperService (comparer et comparer au constructeur Java WallpaperService.Engine(), qui ne prend aucun paramètre).

Un exemple de dérivation d’une classe interne est CubeWallpaper.CubeEngine :

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

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

Notez comment CubeWallpaper.CubeEngine est imbriqué dans CubeWallpaper, CubeWallpaper hérite de la classe conteneur de WallpaperService.Engineet CubeWallpaper.CubeEngine a un constructeur qui prend le type déclarant ( CubeWallpaper dans ce cas) tout comme spécifié ci-dessus.

Interfaces

Les interfaces Java peuvent contenir trois ensembles de membres, dont deux provoquent des problèmes à partir de C# :

  1. Méthodes

  2. Types

  3. Champs

Les interfaces Java sont traduites en deux types :

  1. Interface (facultative) contenant des déclarations de méthode. Cette interface porte le même nom que l’interface Java, sauf qu’elle a également un préfixe « I ».

  2. Classe statique (facultative) contenant tous les champs déclarés dans l’interface Java.

Les types imbriqués sont « déplacés » pour être frères de l’interface englobante au lieu des types imbriqués, avec le nom de l’interface englobante comme préfixe.

Par exemple, prenons l’interface android.os.Parcelable . L’interface Parcelable contient des méthodes, des types imbriqués et des constantes. Les méthodes d’interface Parcelable sont placées dans l’interface Android.OS.IParcelable . Les constantes d’interface Parcelable sont placées dans le type Android.OS.ParcelableConsts . Les types T imbriqués android.os.Parcelable.ClassLoaderCreator<T> et android.os.Parcelable.Creator<ne sont actuellement pas liés en raison des limitations de notre prise en charge des génériques ; s’ils étaient pris en charge, ils seraient présents en tant qu’interfaces Android.OS.IParcelableClassLoaderCreator et Android.OS.IParcelableCreator.> Par exemple, l’interface android.os.IBinder.DeathRecipient imbriquée est liée en tant qu’interface Android.OS.IBinderDeathRecipient .

Notes

À compter de Xamarin.Android 1.9, les constantes d’interface Java sont dupliquées afin de simplifier le portage du code Java. Cela permet d’améliorer le portage du code Java qui s’appuie sur les constantes d’interface du fournisseur Android .

En plus des types ci-dessus, il y a quatre autres modifications :

  1. Un type portant le même nom que l’interface Java est généré pour contenir des constantes.

  2. Les types contenant des constantes d’interface contiennent également toutes les constantes provenant d’interfaces Java implémentées.

  3. Toutes les classes qui implémentent une interface Java contenant des constantes obtiennent un nouveau type InterfaceConsts imbriqué qui contient des constantes de toutes les interfaces implémentées.

  4. Le type Consts est désormais obsolète.

Pour l’interface android.os.Parcelable , cela signifie qu’il y aura désormais un type Android.OS.Parcelable pour contenir les constantes. Par exemple, la constante Parcelable.CONTENTS_FILE_DESCRIPTOR sera liée en tant que constante Parcelable.ContentsFileDescriptor , au lieu de la constante ParcelableConsts.ContentsFileDescriptor .

Pour les interfaces contenant des constantes qui implémentent d’autres interfaces contenant encore plus de constantes, l’union de toutes les constantes est maintenant générée. Par exemple, l’interface android.provider.MediaStore.Video.VideoColumns implémente l’interface android.provider.MediaStore.MediaColumns . Toutefois, avant la version 1.9, le type Android.Provider.MediaStore.Video.VideoColumnsConsts n’a aucun moyen d’accéder aux constantes déclarées sur Android.Provider.MediaStore.MediaColumnsConsts. Par conséquent, l’expression Java MediaStore.Video.VideoColumns.TITLE doit être liée à l’expression C# MediaStore.Video.MediaColumnsConsts.Title qui est difficile à découvrir sans lire beaucoup de documentation Java. Dans la version 1.9, l’expression C# équivalente sera MediaStore.Video.VideoColumns.Title.

En outre, considérez le type android.os.Bundle , qui implémente l’interface Java Parcelable . Étant donné qu’elle implémente l’interface, toutes les constantes de cette interface sont accessibles « via » le type bundle, par exemple , Bundle.CONTENTS_FILE_DESCRIPTOR est une expression Java parfaitement valide. Auparavant, pour porter cette expression vers C#, vous deviez examiner toutes les interfaces implémentées pour voir d’où provenait le CONTENTS_FILE_DESCRIPTOR . À compter de Xamarin.Android 1.9, les classes implémentant des interfaces Java qui contiennent des constantes auront un type InterfaceConsts imbriqué, qui contiendra toutes les constantes d’interface héritées. Cela permet de traduire Bundle.CONTENTS_FILE_DESCRIPTOR en Bundle.InterfaceConsts.ContentsFileDescriptor.

Enfin, les types avec un suffixe Consts comme Android.OS.ParcelableConsts sont désormais obsolètes , autres que les types imbriqués InterfaceConsts récemment introduits. Ils seront supprimés dans Xamarin.Android 3.0.

Ressources

Les images, les descriptions de disposition, les objets blob binaires et les dictionnaires de chaînes peuvent être inclus dans votre application en tant que fichiers de ressources. Diverses API Android sont conçues pour fonctionner sur les ID de ressource au lieu de traiter directement des images, des chaînes ou des objets blob binaires.

Par exemple, un exemple d’application Android qui contient une disposition d’interface utilisateur ( main.axml), une chaîne de table d’internationalisation ( strings.xml) et certaines icônes ( drawable-*/icon.png) conserverait ses ressources dans le répertoire « Resources » de l’application :

Resources/
    drawable-hdpi/
        icon.png

    drawable-ldpi/
        icon.png

    drawable-mdpi/
        icon.png

    layout/
        main.axml

    values/
        strings.xml

Les API Android natives ne fonctionnent pas directement avec les noms de fichiers, mais fonctionnent plutôt sur les ID de ressource. Lorsque vous compilez une application Android qui utilise des ressources, le système de génération empaquetage les ressources pour la distribution et génère une classe appelée Resource qui contient les jetons pour chacune des ressources incluses. Par exemple, pour la disposition Ressources ci-dessus, voici ce que la classe R exposerait :

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;
    }
}

Vous utiliseriez Resource.Drawable.icon ensuite pour référencer le drawable/icon.png fichier, ou Resource.Layout.main pour référencer le layout/main.xml fichier, ou Resource.String.first_string pour référencer la première chaîne dans le fichier values/strings.xmlde dictionnaire .

Constantes et énumérations

Les API Android natives ont de nombreuses méthodes qui prennent ou retournent un int qui doit être mappé à un champ constant pour déterminer ce que signifie int. Pour utiliser ces méthodes, l’utilisateur doit consulter la documentation pour voir quelles constantes sont des valeurs appropriées, ce qui est inférieur à l’idéal.

Par exemple, considérez Activity.requestWindowFeature(int featureID).

Dans ce cas, nous nous efforçons de regrouper les constantes associées dans une énumération .NET et de remapper la méthode pour prendre l’énumération à la place. En procédant ainsi, nous sommes en mesure de proposer une sélection IntelliSense des valeurs potentielles.

L’exemple ci-dessus devient : Activity.RequestWindowFeature(WindowFeatures featureId).

Notez qu’il s’agit d’un processus très manuel pour déterminer quelles constantes appartiennent ensemble et quelles API consomment ces constantes. Veuillez enregistrer des bogues pour toutes les constantes utilisées dans l’API qui seraient mieux exprimées sous forme d’énumération.