Principi di progettazione dell'API Xamarin.Android

Oltre alle principali librerie di classi di base che fanno parte di Mono, Xamarin.Android include associazioni per varie API Android per consentire agli sviluppatori di creare applicazioni Android native con Mono.

Al centro di Xamarin.Android è disponibile un motore di interoperabilità che collega il mondo C# al mondo Java e fornisce agli sviluppatori l'accesso alle API Java da C# o da altri linguaggi .NET.

Principi di progettazione

Questi sono alcuni dei principi di progettazione per l'associazione Xamarin.Android

  • Conforme alle linee guida per la progettazione di .NET Framework.

  • Consentire agli sviluppatori di sottoclassare le classi Java.

  • La sottoclasse deve funzionare con costrutti standard C#.

  • Derivare da una classe esistente.

  • Chiamare il costruttore di base per concatenare.

  • È necessario eseguire l'override dei metodi con il sistema di override di C#.

  • Semplifica le attività Java comuni e semplifica le attività Java.

  • Esporre le proprietà JavaBean come proprietà C#.

  • Esporre un'API fortemente tipizzata:

    • Aumentare la sicurezza dei tipi.

    • Ridurre al minimo gli errori di runtime.

    • Ottenere intellisense dell'IDE sui tipi restituiti.

    • Consente la documentazione popup dell'IDE.

  • Incoraggiare l'esplorazione in-IDE delle API:

    • Usare le alternative del framework per ridurre al minimo l'esposizione di Java Classlib.

    • Esporre delegati C# (lambda, metodi anonimi e System.Delegate) anziché interfacce a metodo singolo quando appropriato e applicabile.

    • Fornire un meccanismo per chiamare librerie Java arbitrarie ( Android.Runtime.JNIEnv).

Assembly

Xamarin.Android include diversi assembly che costituiscono il profilo MonoMobile. La pagina Assembly contiene altre informazioni.

Le associazioni alla piattaforma Android sono contenute nell'assembly Mono.Android.dll . Questo assembly contiene l'intera associazione per l'utilizzo delle API Android e la comunicazione con la macchina virtuale di runtime Android.

Progettazione binding

Raccolte

Le API Android usano ampiamente le raccolte java.util per fornire elenchi, set e mappe. Questi elementi vengono esposti usando le interfacce System.Collections.Generic nell'associazione. I mapping fondamentali sono:

Sono state fornite classi helper per facilitare il marshalling copyless più rapido di questi tipi. Quando possibile, è consigliabile usare queste raccolte fornite anziché l'implementazione fornita dal framework, ad esempio List<T> o Dictionary<TKey, TValue>. Le implementazioni di Android.Runtime usano internamente una raccolta Java nativa e pertanto non richiedono la copia da e verso una raccolta nativa quando si passa a un membro dell'API Android.

È possibile passare qualsiasi implementazione dell'interfaccia a un metodo Android accettando tale interfaccia, ad esempio passare un List<int> oggetto al costruttore ArrayAdapter<int>(Context, int, IList<int>). Tuttavia, per tutte le implementazioni ad eccezione delle implementazioni di Android.Runtime, ciò comporta la copia dell'elenco dalla macchina virtuale Mono nella macchina virtuale di runtime Android. Se l'elenco viene modificato in un secondo momento all'interno del runtime Android ,ad esempio richiamando ArrayAdapter<T>. Metodo Add(T), tali modifiche non saranno visibili nel codice gestito. Se fosse stato usato un oggetto JavaList<int> , tali modifiche sarebbero visibili.

Riformulate, implementazioni dell'interfaccia delle raccolte che non sono una delle classi helperelencate sopra elencate es marshalling solo [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.");

Proprietà

I metodi Java vengono trasformati in proprietà, se appropriato:

  • La coppia T getFoo() di metodi Java e void setFoo(T) viene trasformata nella Foo proprietà . Esempio: Activity.Intent.

  • Il metodo getFoo() Java viene trasformato nella proprietà Foo di sola lettura. Esempio: Context.PackageName.

  • Le proprietà di sola impostazione non vengono generate.

  • Le proprietà non vengono generate se il tipo di proprietà è una matrice.

Eventi e listener

Le API Android sono basate su Java e i relativi componenti seguono il modello Java per associare listener di eventi. Questo modello tende a essere complesso perché richiede all'utente di creare una classe anonima e dichiarare i metodi di cui eseguire l'override, ad esempio, questo è il modo in cui le operazioni verrebbero eseguite in Android con 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!");
    }
});

Il codice equivalente in C# che usa eventi è:

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

Si noti che entrambi i meccanismi precedenti sono disponibili con Xamarin.Android. È possibile implementare un'interfaccia del listener e collegarla con View.SetOnClickListener oppure collegare un delegato creato tramite uno dei consueti paradigmi C# all'evento Click.

Quando il metodo di callback del listener ha un risultato void, vengono creati elementi API basati su un delegato TEventArgs> EventHandler<. Viene generato un evento simile all'esempio precedente per questi tipi di listener. Tuttavia, se il callback del listener restituisce un valore non void e non booleano , gli eventi e i gestori eventi non vengono utilizzati. Viene invece generato un delegato specifico per la firma del callback e si aggiungono proprietà anziché eventi. Il motivo è gestire l'ordine di chiamata del delegato e la gestione dei resi. Questo approccio rispecchia le operazioni eseguite con l'API Xamarin.iOS.

Gli eventi o le proprietà C# vengono generati automaticamente solo se il metodo di registrazione eventi Android:

  1. Ha un set prefisso, ad esempio impostareOnClickListener.

  2. Ha un void tipo restituito.

  3. Accetta un solo parametro, il tipo di parametro è un'interfaccia, l'interfaccia ha un solo metodo e il nome dell'interfaccia termina in Listener , ad esempio View.OnClick Listener.

Inoltre, se il metodo di interfaccia listener ha un tipo restituito booleano anziché void, la sottoclasse EventArgs generata conterrà una proprietà Handled. Il valore della proprietà Handled viene usato come valore restituito per il metodo Listener e per impostazione predefinita è true.

Ad esempio, il metodo Android View.setOnKeyListener() accetta l'interfaccia View.OnKeyListener e il metodo View.OnKeyListener.onKey(View, int, KeyEvent) ha un tipo restituito booleano. Xamarin.Android genera un evento View.KeyPress corrispondente, ovvero eventHandler <View.KeyEventArgs>. La classe KeyEventArgs ha a sua volta una proprietà View.KeyEventArgs.Handled, utilizzata come valore restituito per il metodo View.OnKeyListener.onKey().

Si prevede di aggiungere overload per altri metodi e ctor per esporre la connessione basata su delegato. Inoltre, i listener con più callback richiedono un'ispezione aggiuntiva per determinare se l'implementazione di singoli callback è ragionevole, quindi stiamo convertendo questi dati man mano che vengono identificati. Se non è presente alcun evento corrispondente, i listener devono essere usati in C#, ma è consigliabile portare a qualsiasi utente che potrebbe delegare l'utilizzo all'attenzione. Abbiamo anche eseguito alcune conversioni di interfacce senza il suffisso "Listener" quando era chiaro che trarrebbero vantaggio da un'alternativa del delegato.

Tutte le interfacce listener implementano Android.Runtime.IJavaObject interfaccia, a causa dei dettagli di implementazione dell'associazione, pertanto le classi listener devono implementare questa interfaccia. A tale scopo, è possibile implementare l'interfaccia del listener in una sottoclasse di Java.Lang.Object o qualsiasi altro oggetto Java di cui è stato eseguito il wrapping, ad esempio un'attività Android.

Eseguibili

Java usa l'interfaccia java.lang.Runnable per fornire un meccanismo di delega. La classe java.lang.Thread è un consumer notevole di questa interfaccia. Android ha usato anche l'interfaccia nell'API. Activity.runOnUiThread() e View.post() sono esempi rilevanti.

L'interfaccia Runnable contiene un singolo metodo void, run(). Si presta quindi all'associazione in C# come delegato System.Action . Sono stati forniti overload nell'associazione che accettano un Action parametro per tutti i membri dell'API che usano un Runnable nell'API nativa, ad esempio Activity.RunOnUiThread() e View.Post().

Sono stati lasciati eseguiti gli overload IRunnable anziché sostituirli perché diversi tipi implementano l'interfaccia e possono quindi essere passati direttamente come eseguibili.

Classi interne

Java ha due tipi diversi di classi annidate: classi annidate statiche e classi non statiche.

Le classi annidate statiche Java sono identiche ai tipi annidati C#.

Le classi annidate non statiche, dette anche classi interne, sono notevolmente diverse. Contengono un riferimento implicito a un'istanza del tipo di inclusione e non possono contenere membri statici( tra le altre differenze all'esterno dell'ambito di questa panoramica).

Per quanto riguarda l'associazione e l'uso di C#, le classi annidate statiche vengono considerate come tipi annidati normali. Le classi interne, nel frattempo, presentano due differenze significative:

  1. Il riferimento implicito al tipo contenitore deve essere fornito in modo esplicito come parametro del costruttore.

  2. Quando eredita da una classe interna, la classe interna deve essere annidata all'interno di un tipo che eredita dal tipo contenitore della classe interna di base e il tipo derivato deve fornire un costruttore dello stesso tipo del tipo contenitore C#.

Si consideri ad esempio la classe interna Android.Service.Wallpaper.WallpaperService.Engine . Poiché si tratta di una classe interna, il costruttore WallpaperService.Engine() accetta un riferimento a un'istanza di WallpaperService (confronto e contrasto con il costruttore Java WallpaperService.Engine(), che non accetta parametri.

Una derivazione di esempio di una classe interna è CubeWallpaper.CubeEngine:

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

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

Si noti come CubeWallpaper.CubeEngine viene annidato all'interno CubeWallpaperdi , CubeWallpaper eredita dalla classe contenitore di WallpaperService.Enginee CubeWallpaper.CubeEngine ha un costruttore che accetta il tipo dichiarante , CubeWallpaper in questo caso , tutto come specificato in precedenza.

Interfacce

Le interfacce Java possono contenere tre set di membri, due dei quali causano problemi da C#:

  1. Metodi

  2. Tipi

  3. Campi

Le interfacce Java vengono convertite in due tipi:

  1. Interfaccia (facoltativa) contenente le dichiarazioni di metodo. Questa interfaccia ha lo stesso nome dell'interfaccia Java, ad eccezione del prefisso ' I '.

  2. Classe statica (facoltativa) contenente tutti i campi dichiarati all'interno dell'interfaccia Java.

I tipi annidati vengono "rilocati" come elementi di pari livello dell'interfaccia di inclusione anziché dei tipi annidati, con il nome dell'interfaccia contenitore come prefisso.

Si consideri ad esempio l'interfaccia android.os.Parcelable . L'interfaccia Parcelable contiene metodi, tipi annidati e costanti. I metodi di interfaccia Parcelable vengono inseriti nell'interfaccia Android.OS.IParcelable . Le costanti dell'interfaccia Parcelable vengono inserite nel tipo Android.OS.ParcelableConsts . I tipi android.os.Parcelable.ClassLoaderCreator T> e android.os.Parcelable.Creator<T> annidati non sono attualmente associati a causa delle limitazioni del supporto dei generics. Se fossero supportati, verrebbero presenti come interfacce Android.OS.IParcelableClassLoaderCreator e Android.OS.IParcelableCreator.< Ad esempio, l'interfaccia android.os.IBinder.DeathRecipient annidata è associata come interfaccia Android.OS.IBinderDeathRecipient.

Nota

A partire da Xamarin.Android 1.9, le costanti dell'interfaccia Java vengono duplicate nel tentativo di semplificare la conversione del codice Java. Ciò consente di migliorare la conversione del codice Java che si basa sulle costanti dell'interfaccia del provider Android.

Oltre ai tipi precedenti, sono state apportate altre quattro modifiche:

  1. Viene generato un tipo con lo stesso nome dell'interfaccia Java per contenere costanti.

  2. I tipi contenenti costanti di interfaccia contengono anche tutte le costanti provenienti dalle interfacce Java implementate.

  3. Tutte le classi che implementano un'interfaccia Java contenente costanti ottengono un nuovo tipo InterfaceConsts annidato che contiene costanti di tutte le interfacce implementate.

  4. Il tipo Consts è ora obsoleto.

Per l'interfaccia android.os.Parcelable, questo significa che ora sarà presente un tipo Android.OS.Parcelable per contenere le costanti. Ad esempio, la costante Parcelable.CONTENTS_FILE_DESCRIPTOR verrà associata come costante Parcelable.ContentsFileDescriptor anziché come costante ParcelableConsts.ContentsFileDescriptor.

Per le interfacce contenenti costanti che implementano altre interfacce contenenti ancora più costanti, viene ora generata l'unione di tutte le costanti. Ad esempio, l'interfaccia android.provider.MediaStore.Video.VideoColumns implementa l'interfaccia android.provider.MediaStore.MediaColumns . Tuttavia, prima della versione 1.9, il tipo Android.Provider.MediaStore.Video.VideoColumnsConsts non ha modo di accedere alle costanti dichiarate in Android.Provider.MediaStore.MediaColumnsConsts. Di conseguenza, l'espressione Java MediaStore.Video.VideoColumns.TITLE deve essere associata all'espressione C# MediaStore.Video.MediaColumnsConsts.Title, difficile da individuare senza leggere un sacco di documentazione Java. Nella versione 1.9 l'espressione C# equivalente sarà MediaStore.Video.VideoColumns.Title.

Si consideri anche il tipo android.os.Bundle , che implementa l'interfaccia Java Parcelable . Poiché implementa l'interfaccia, tutte le costanti su tale interfaccia sono accessibili tramite il tipo bundle, ad esempio Bundle.CONTENTS_FILE_DESCRIPTOR è un'espressione Java perfettamente valida. In precedenza, per convertire questa espressione in C# è necessario esaminare tutte le interfacce implementate per vedere da quale tipo proviene il CONTENTS_FILE_DESCRIPTOR . A partire da Xamarin.Android 1.9, le classi che implementano interfacce Java che contengono costanti avranno un tipo InterfaceConsts annidato, che conterrà tutte le costanti dell'interfaccia ereditate. Ciò consentirà la conversione di Bundle.CONTENTS_FILE_DESCRIPTOR in Bundle.InterfaceConsts.ContentsFileDescriptor.

Infine, i tipi con un suffisso Consts , ad esempio Android.OS.ParcelableConsts , sono ora Obsoleti, oltre ai nuovi tipi annidati InterfaceConsts. Verranno rimossi in Xamarin.Android 3.0.

Risorse

Le immagini, le descrizioni del layout, i BLOB binari e i dizionari di stringhe possono essere inclusi nell'applicazione come file di risorse. Diverse API Android sono progettate per operare sugli ID risorsa anziché gestire direttamente immagini, stringhe o BLOB binari.

Ad esempio, un'app Android di esempio che contiene un layout dell'interfaccia utente ( main.axml), una stringa di tabella di internazionalizzazione ( strings.xml) e alcune icone ( drawable-*/icon.png) manterranno le relative risorse nella directory "Resources" dell'applicazione:

Resources/
    drawable-hdpi/
        icon.png

    drawable-ldpi/
        icon.png

    drawable-mdpi/
        icon.png

    layout/
        main.axml

    values/
        strings.xml

Le API Android native non funzionano direttamente con i nomi file, ma operano invece sugli ID risorsa. Quando si compila un'applicazione Android che usa le risorse, il sistema di compilazione crea un pacchetto delle risorse per la distribuzione e genera una classe denominata Resource che contiene i token per ognuna delle risorse incluse. Ad esempio, per il layout risorse precedente, si tratta di ciò che la classe R espone:

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

Si userà Resource.Drawable.icon quindi per fare riferimento al drawable/icon.png file o Resource.Layout.main per fare riferimento layout/main.xml al file oppure Resource.String.first_string per fare riferimento alla prima stringa nel file values/strings.xmldel dizionario .

Costanti ed enumerazioni

Le API Android native hanno molti metodi che accettano o restituiscono un valore int di cui è necessario eseguire il mapping a un campo costante per determinare il significato int. Per usare questi metodi, l'utente deve consultare la documentazione per vedere quali costanti sono valori appropriati, che è minore dell'ideale.

Si consideri ad esempio Activity.requestWindowFeature(int featureID).

In questi casi si tenta di raggruppare le costanti correlate in un'enumerazione .NET e di rieseguire il mapping del metodo per accettare invece l'enumerazione. In questo modo, è possibile offrire la selezione intelliSense dei valori potenziali.

L'esempio precedente diventa Activity.RequestWindowFeature(WindowFeatures featureId).

Si noti che si tratta di un processo molto manuale per determinare quali costanti appartengono insieme e quali API utilizzano queste costanti. Segnalare bug per le costanti usate nell'API che sarebbero espresse meglio come enumerazione.