Compartir vía


Principios de diseño de API de Xamarin.Android

Además de las bibliotecas de clases base básicas que forman parte de Mono, Xamarin.Android incluye enlaces para varias API de Android que permiten a los desarrolladores crear aplicaciones nativas de Android con Mono.

En el núcleo de Xamarin.Android hay un motor de interoperabilidad que enlaza el entorno de C# con el entorno de Java y proporciona a los desarrolladores acceso a las API de Java desde C# u otros lenguajes .NET.

Principios de diseño

Estos son algunos de nuestros principios de diseño para el enlace de Xamarin.Android

  • Conforme a lasGuías de diseño de .NET Framework.

  • Permitir a los desarrolladores subclase clases de Java.

  • La subclase debe funcionar con construcciones estándar de C#.

  • Derive de una clase existente.

  • Llame al constructor base para encadenar.

  • La invalidación de métodos debe realizarse con el sistema de invalidación de C#.

  • Haga que las tareas comunes de Java sean fáciles y difíciles.

  • Exponga las propiedades de JavaBean como propiedades de C#.

  • Exponga una API fuertemente tipada:

    • Aumente con seguridad de tipos.

    • Minimice los errores en tiempo de ejecución.

    • Obtenga intellisense del IDE en los tipos de valor devuelto.

    • Permite la documentación emergente del IDE.

  • Aliente la exploración en el IDE de las API:

    • Utilice alternativas de marco para minimizar la exposición de Classlib de Java.

    • Exponga delegados de C# (lambdas, métodos anónimos y System.Delegate) en lugar de interfaces de método único cuando sea adecuado y aplicable.

    • Proporcione un mecanismo para llamar a bibliotecas arbitrarias de Java (Android.Runtime.JNIEnv).

Ensamblados

Xamarin.Android incluye una serie de ensamblados que constituyen el Perfil MonoMobile. La página Ensamblados tiene más información.

Los enlaces a la plataforma Android se encuentran en el ensamblado Mono.Android.dll. Este ensamblado contiene todo el enlace para consumir las API de Android y comunicarse con la máquina virtual en tiempo de ejecución de Android.

Diseño de enlace

Colecciones

Las API de Android usan las colecciones java.util ampliamente para proporcionar listas, conjuntos y mapas. Exponemos estos elementos mediante las interfaces de System.Collections.Generic en nuestro enlace. Las asignaciones fundamentales son:

Hemos proporcionado clases de asistencia para facilitar la serialización sin copia más rápida de estos tipos. Cuando sea posible, se recomienda usar estas colecciones proporcionadas en lugar de la implementación proporcionada del marco, como List<T> o Dictionary<TKey, TValue>. Las implementaciones de Android.Runtime usan internamente una colección de Java nativa y, por lo tanto, no requieren copiar en una colección nativa ni desde ella al pasar a un miembro de la API de Android.

Puede pasar cualquier implementación de interfaz a un método Android que acepte esa interfaz, por ejemplo, pasar un List<int> al ArrayAdapter<int>(Contexto, int, IList<int>) constructor. Sin embargo,, para todas las implementaciones excepto para las implementaciones de Android.Runtime, esto implica copiar la lista de la máquina virtual mono en la máquina virtual en tiempo de ejecución de Android. Si la lista se cambia más adelante en el entorno de ejecución de Android (por ejemplo, invocando el ArrayAdapter<T>. Add(T) método), esos cambios no estarán visibles en código administrado. Si se usara un JavaList<int>, esos cambios serían visibles.

Implementaciones de interfaz de colecciones que se no una de las clase de asistencia enumerada anteriormentesolo serializar [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.");

Propiedades

Los métodos de Java se transforman en propiedades, cuando proceda:

  • El par de métodos de Java T getFoo() y void setFoo(T) se transforman en la propiedad Foo. Ejemplo: Actividad.Intent.

  • El método Java getFoo() se transforma en la propiedad Foo de solo lectura. Ejemplo: Context.PackageName.

  • No se generan propiedades de solo establecimiento.

  • Las propiedades no se genera si el tipo de propiedad sería una matriz.

Eventos y agentes de escucha

Las API de Android se basan en Java y sus componentes siguen el patrón de Java para enlazar agentes de escucha de eventos. Este patrón tiende a ser complicado, ya que requiere que el usuario cree una clase anónima y declare los métodos para invalidar, por ejemplo, así es como se harían las cosas en 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!");
    }
});

El código equivalente en C# que usa eventos sería:

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

Tenga en cuenta que ambos mecanismos anteriores están disponibles con Xamarin.Android. Puede implementar una interfaz de agente de escucha y adjuntarla con View.SetOnClickListener, o bien puede adjuntar un delegado creado a través de cualquiera de los paradigmas habituales de C# al evento Click.

Cuando el método de devolución de llamada del agente de escucha tiene un valor devuelto nulo, creamos elementos de API basados en un delegado de EventHandler<TEventArgs>. Generamos un evento como el ejemplo anterior para estos tipos de agente de escucha. Sin embargo, si la devolución de llamada del agente de escucha devuelve un valor de booleano, que no es nulo y no, no se usan eventos ni EventHandlers. En su lugar, generamos un delegado específico para la firma de la devolución de llamada y agregamos propiedades en lugar de eventos. El motivo es tratar con el orden de invocación del delegado y el control de devolución. Este enfoque refleja lo que se hace con la API de Xamarin.iOS.

Los eventos o propiedades de C# solo se generan automáticamente si el método de registro de eventos de Android:

  1. Tiene un prefijo set, por ejemplo, establecidoOnClickListener.

  2. Tiene un tipo de valor devuelto void.

  3. Acepta solo un parámetro, el tipo de parámetro es una interfaz, la interfaz solo tiene un método y el nombre de la interfaz termina en Listener, por ejemplo, View.OnClick Listener.

Además, si el método de interfaz cliente de escucha tiene un tipo de valor devuelto de booleano en lugar de void, la subclase EventArgs generada contendrá una propiedad Manipulado. El valor de propiedad Manipulado se usa como valor devuelto para el método Listener y el valor predeterminado es true.

Por ejemplo, el método View.setOnKeyListener() de Android acepta la interfaz View.OnKeyListener y el método View.OnKeyListener.onKey(View, int, KeyEvent ) tiene un tipo de valor devuelto booleano. Xamarin.Android genera un evento View.KeyPress correspondiente, que es un evento EventHandler<View.KeyEventArgs>. A su vez, la clase KeyEventArgs tiene una propiedad View.KeyEventArgs.Handled, que se usa como valor devuelto para el método View.OnKeyListener.onKey().

Tenemos previsto agregar sobrecargas para otros métodos y ctors para exponer la conexión basada en delegados. Además, los agentes de escucha con varias devoluciones de llamada requieren una inspección adicional para determinar si la implementación de devoluciones de llamada individuales es razonable, por lo que estamos convirtiendo estas a medida que se identifican. Si no hay ningún evento correspondiente, los agentes de escucha deben usarse en C#, pero por favor traiga lo que piense que podría tener el uso delegado a nuestra atención. También hemos realizado algunas conversiones de interfaces sin el sufijo "Cliente de escucha" cuando estaba claro que se beneficiarían de una alternativa de delegado.

Todas las interfaces de los cliente de escucha implementan Android.Runtime.IJavaObject interfaz, debido a los detalles de implementación del enlace, por lo que las clases de agente de escucha deben implementar esta interfaz. Esto se puede hacer mediante la implementación de la interfaz del agente de escucha en una subclase de Java.Lang.Object o cualquier otro objeto java encapsulado, como una actividad de Android.

Ejecutables

Java utiliza la interfaz de java.lang.Runnable se para proporcionar un mecanismo de delegación. La clase java.lang.Thread es un consumidor notable de esta interfaz. Android también ha empleado la interfaz en la API. Activity.runOnUiThread() y View.post() son ejemplos importantes.

La interfaz Runnable contiene un único método void, run(). Por lo tanto, se presta a enlazar en C# como delegado System.Action. Hemos proporcionado sobrecargas en el enlace que aceptan un parámetro Action para todos los miembros de la API que consumen un Runnable en la API nativa, por ejemplo, Activity.RunOnUiThread() y View.Post().

Dejamos las sobrecargas de IRunnable en lugar de reemplazarlas, ya que varios tipos implementan la interfaz y, por tanto, se pueden pasar como ejecutables directamente.

Clases internas

Java tiene dos tipos diferentes de clases anidadas: clases anidadas estáticas y clases no estáticas.

Las clases anidadas estáticas de Java son idénticas a los tipos anidados de C#.

Las clases anidadas no estáticas, también llamadas clases internas, son significativamente diferentes. Contienen una referencia implícita a una instancia de su tipo envolvente y no pueden contener miembros estáticos (entre otras diferencias fuera del ámbito de esta introducción).

Cuando se trata de enlazar y usar C#, las clases anidadas estáticas se tratan como tipos anidados normales. Las clases internas, mientras tanto, tienen dos diferencias significativas:

  1. La referencia implícita al tipo contenedor debe proporcionarse explícitamente como parámetro de constructor.

  2. Al heredar de una clase interna, la clase interna debe anidarse dentro de un tipo que herede del tipo contenedor de la clase interna base y el tipo derivado debe proporcionar un constructor del mismo tipo que el tipo contenedor de C#.

Por ejemplo, considere la clase interna Android.Service.Wallpaper.WallpaperService.Engine. Dado que es una clase interna, el constructor WallpaperService.Engine() toma una referencia a una instancia de WallpaperService (compare y contrasta con el constructor Java WallpaperService.Engine(), que no toma parámetros.

Una derivación de ejemplo de una clase interna es CubeWallpaper.CubeEngine:

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

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

Tenga en cuenta cómo CubeWallpaper.CubeEngine está anidado dentro de CubeWallpaper, CubeWallpaper hereda de la clase contenedora de WallpaperService.Enginey CubeWallpaper.CubeEngine tiene un constructor que toma el tipo declarante, CubeWallpaper en este caso, todo como se especificó anteriormente.

Interfaces

Las interfaces de Java pueden contener tres conjuntos de miembros, dos de los cuales provocan problemas de C#:

  1. Métodos

  2. Tipos

  3. Fields

Las interfaces de Java se traducen en dos tipos:

  1. Interfaz (opcional) que contiene declaraciones de método. Esta interfaz tiene el mismo nombre que la interfaz de Java, excepto también tiene un prefijo " I ".

  2. Una clase estática (opcional) que contiene los campos declarados en la interfaz de Java.

Los tipos anidados se "reubican" para ser elementos del mismo nivel de la interfaz envolvente en lugar de los tipos anidados, con el nombre de la interfaz envolvente como prefijo.

Por ejemplo, considere la interfaz android.os.Parcelable. La interfaz Parcelable contiene métodos, tipos anidados y constantes. Los métodos de interfaz Parcelable se colocan en la interfaz Android.OS.IParcelable. Las constantes de interfaz Parcelable se colocan en el tipo Android.OS.ParcelableConsts. Los tipos anidados android.os.Parcelable.ClassLoaderCreator<T> y android.os.Parcelable.Creator<> no están enlazados actualmente debido a limitaciones en nuestra compatibilidad con genéricos; si fueran compatibles, estarían presentes como las interfaces Android.OS.IParcelableClassLoaderCreator y Android.OS.IParcelableCreator. Por ejemplo, la interfaz anidada android.os.IBinder.DeathRecipient está enlazado como la interfaz Android.OS.IBinderDeathRecipient .

Nota:

A partir de Xamarin.Android 1.9, las constantes de interfaz de Java se duplicados en un esfuerzo por simplificar la portabilidad de código Java. Esto ayuda a mejorar la portabilidad del código Java que se basa en proveedor de Android constantes de interfaz.

Además de los tipos anteriores, hay cuatro cambios adicionales:

  1. Se genera un tipo con el mismo nombre que la interfaz de Java para contener constantes.

  2. Los tipos que contienen constantes de interfaz también contienen todas las constantes que proceden de interfaces Java implementadas.

  3. Todas las clases que implementan una interfaz Java que contiene constantes reciben un nuevo tipo anidado InterfaceConsts que contiene constantes de todas las interfaces implementadas.

  4. El tipo de Consts ahora está obsoleto.

Para la interfaz android.os.Parcelable, esto significa que ahora habrá un tipo Android.OS.Parcelable para contener las constantes. Por ejemplo, la constante Parcelable.CONTENTS_FILE_DESCRIPTOR se enlazará como constante Parcelable.ContentsFileDescriptor, en lugar de como la constante ParcelableConsts.ContentsFileDescriptor.

En el caso de las interfaces que contienen constantes que implementan otras interfaces que aún contienen más constantes, ahora se genera la unión de todas las constantes. Por ejemplo, la interfaz android.provider.MediaStore.Video.VideoColumns implementa la interfaz android.provider.MediaStore.MediaColumns. Sin embargo, antes de la versión 1.9, el tipo Android.Provider.MediaStore.Video.VideoColumnsConsts no tiene forma de acceder a las constantes declaradas en Android.Provider.MediaStore.MediaColumnsConsts. Como resultado, la expresión de Java MediaStore.Video.VideoColumns.TITLE debe enlazarse a la expresión de C# MediaStore.Video.MediaColumnsConsts.Title que es difícil de detectar sin leer una gran cantidad de documentación de Java. En la versión 1.9, la expresión de C# equivalente será MediaStore.Video.VideoColumns.Title.

Además, considere el tipo android.os.Bundle, que implementa la interfaz de JavaParcelable. Puesto que implementa la interfaz, todas las constantes de esa interfaz son accesibles "a través" del tipo Bundle, por ejemplo , Agrupación.CONTENTS_FILE_DESCRIPTOR es una expresión Java perfectamente válida. Anteriormente, para migrar esta expresión a C#, tendría que examinar todas las interfaces que se implementan para ver desde qué tipo procede el CONTENTS_FILE_DESCRIPTOR. A partir de Xamarin.Android 1.9, las clases que implementan interfaces de Java que contienen constantes tendrán un tipo InterfaceConsts anidado, que contendrá todas las constantes de interfaz heredadas. Esto permitirá traducir Bundle.CONTENTS_FILE_DESCRIPTOR a Bundle.InterfaceConsts.ContentsFileDescriptor.

Por último, los tipos con un Consts sufijo, como Android.OS.ParcelableConsts ahora están obsoletos, aparte de los tipos anidados InterfaceConsts recién introducidos. Se quitarán en Xamarin.Android 3.0.

Recursos

Las imágenes, las descripciones de diseño, los blobs binarios y los diccionarios de cadenas se pueden incluir en la aplicación como archivos de recursos. Varias API de Android están diseñadas para operar en los identificadores de recursos en lugar de trabajar directamente con imágenes, cadenas o blobs binarios.

Por ejemplo, una aplicación Android de ejemplo que contiene un diseño de interfaz de usuario ( main.axml), una cadena de tabla de internacionalización ( strings.xml) y algunos iconos ( drawable-*/icon.png) mantendrían sus recursos en el directorio "Recursos" de la aplicación:

Resources/
    drawable-hdpi/
        icon.png

    drawable-ldpi/
        icon.png

    drawable-mdpi/
        icon.png

    layout/
        main.axml

    values/
        strings.xml

Las API nativas de Android no funcionan directamente con nombres de archivo, sino que funcionan en identificadores de recursos. Al compilar una aplicación de Android que usa recursos, el sistema de compilación empaquetará los recursos para la distribución y generará una clase denominada Resource que contiene los tokens para cada uno de los recursos incluidos. Por ejemplo, para el diseño de recursos anterior, esto es lo que la clase R expondría:

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

Después, usaría Resource.Drawable.icon para hacer referencia al drawable/icon.png archivo, o Resource.Layout.main para hacer referencia layout/main.xml al archivo, o Resource.String.first_string para hacer referencia a la primera cadena del archivo de diccionariovalues/strings.xml.

Constantes y enumeraciones

Las API nativas de Android tienen muchos métodos que toman o devuelven un valor int que se debe asignar a un campo constante para determinar qué significa int. Para usar estos métodos, es necesario que el usuario consulte la documentación para ver qué constantes son valores adecuados, que es menor que lo ideal.

Por ejemplo, considere Activity.requestWindowFeature(int featureID).

En estos casos, intentamos agrupar constantes relacionadas en una enumeración de .NET y reasignar el método para tomar la enumeración en su lugar. Al hacerlo, podemos ofrecer la selección de IntelliSense de los valores potenciales.

El ejemplo anterior se convierte en: Activity.RequestWindowFeature(WindowFeatures featureId).

Tenga en cuenta que se trata de un proceso muy manual para averiguar qué constantes pertenecen juntas y qué API consumen estas constantes. Envíe mensajes de error sobre las constantes usadas en la API que podrían expresarse mejor como una enumeración.