Parte 4: Trabajo con varias plataformas

Control de la divergencia y características de la plataforma

La divergencia no es solo un problema entre plataformas. Los dispositivos que pertenecen a la "misma" plataforma tienen diferentes funcionalidades (especialmente la amplia variedad de dispositivos Android disponibles). Lo más obvio y básico es el tamaño de la pantalla, pero otros atributos del dispositivo pueden variar y requerir que una aplicación compruebe ciertas funcionalidades y se comporte de forma diferente en función de la presencia (o ausencia) de estas.

Esto significa que todas las aplicaciones necesitan tratar con una degradación correcta de la funcionalidad o, de lo contrario, presentarán un conjunto de características poco atractivo, basado en el mínimo común denominador. La integración profunda de Xamarin con los SDK nativos de cada plataforma permite que las aplicaciones aprovechen la funcionalidad específica de la plataforma, por lo que tiene sentido diseñar aplicaciones que usen esas características.

Consulte la documentación sobre funcionalidades de la plataforma para obtener información general sobre cómo las plataformas difieren en lo que respecta a la funcionalidad.

Ejemplos de divergencia de plataforma

Elementos fundamentales que existen entre plataformas

Hay algunas características de las aplicaciones para dispositivos móviles que son universales. Estos son conceptos generales que, normalmente, son verdaderos para todos los dispositivos y, por lo tanto, pueden formar la base del diseño de la aplicación:

  • Selección de características mediante pestañas o menús
  • Listas de datos y desplazamiento
  • Vistas únicas de datos
  • Edición de vistas únicas de datos
  • Volver atrás

Al diseñar el flujo de pantallas general, puede basar una experiencia de usuario común en estos conceptos.

Atributos específicos de la plataforma

Además de los elementos básicos que existen en todas las plataformas, tendrá que afrontar las diferencias clave entre las plataformas en el diseño. Es posible que deba tener en cuenta estas diferencias (y escribir código específicamente para controlarlas):

  • Tamaños de pantalla: algunas plataformas (como iOS y versiones anteriores de Windows Phone) tienen tamaños de pantalla estandarizados que son relativamente simples de usar como destino. Los dispositivos Android tienen una gran variedad de dimensiones de pantalla, lo que requiere un esfuerzo mayor para que la aplicación lo admita.
  • Metáforas de navegación: difieren entre plataformas (por ejemplo, el botón "atrás" del hardware, el control de la interfaz de usuario panorámica) y dentro de las plataformas (Android 2 y 4, iPhone frente a iPad).
  • Teclados: algunos dispositivos Android tienen teclados físicos, mientras que otros solo tienen un teclado de software. Un código que detecta cuándo un teclado de software está ocultando parte de la pantalla tiene que ser sensible a estas diferencias.
  • Función táctil y gestos: la compatibilidad del sistema operativo con el reconocimiento de gestos varía, especialmente en versiones anteriores de cada sistema operativo. Las versiones anteriores de Android tienen una compatibilidad muy limitada con las operaciones táctiles, lo que significa que la compatibilidad con dispositivos anteriores puede requerir código independiente.
  • Notificaciones push: hay diferentes funcionalidades o implementaciones en cada plataforma (por ejemplo, los iconos dinámicos en Windows).

Características específicas de los dispositivos

Determine cuáles deben ser las características mínimas necesarias para la aplicación o qué características adicionales se van a aprovechar en cada plataforma. Se necesitará código para detectar características y deshabilitar la funcionalidad u ofrecer alternativas (por ejemplo, una alternativa a la geolocalización podría ser permitir que el usuario especifique una ubicación o la elija en un mapa):

  • Cámara: la funcionalidad es diferente entre los dispositivos: algunos dispositivos no tienen ninguna cámara, otros tienen cámaras frontales y traseras. Algunas cámaras pueden grabar vídeo.
  • Geolocalización y mapas: la compatibilidad con la ubicación GPS o Wi-Fi no está presente en todos los dispositivos. Las aplicaciones también deben satisfacer los distintos niveles de precisión que admite cada método.
  • Acelerómetro, giroscopio y brújula: estas características a menudo se encuentran solo en una selección de dispositivos de cada plataforma, por lo que las aplicaciones casi siempre necesitan proporcionar un elemento de reserva cuando no se admite el hardware.
  • Twitter y Facebook: solo están "integrados" en iOS5 e iOS6, respectivamente. En versiones anteriores y en otras plataformas, tendrá que proporcionar sus propias funciones de autenticación e interfaz directamente con la API de cada servicio.
  • Transmisión de datos en proximidad (NFC): solo en teléfonos Android (en algunos) (en el momento de redactar este texto).

Divergencia entre plataformas

Hay dos enfoques diferentes para que se admitan varias plataformas a partir del mismo código base, cada uno con sus propias ventajas y desventajas.

  • Abstracción de plataforma: el patrón de fachada empresarial, proporciona un acceso unificado entre plataformas y abstrae las implementaciones de plataforma particulares en una sola API unificada.
  • Implementación divergente: invocación de características de plataforma específicas a través de implementaciones divergentes mediante herramientas arquitectónicas como interfaces y herencia o compilación condicional.

Abstracción de plataforma

Abstracción de clases

Uso de interfaces o clases base definidas en el código compartido e implementadas o extendidas en proyectos de plataforma específica. Escribir y extender el código compartido con abstracciones de clases es especialmente adecuado para las bibliotecas de clases portables ya que estas tienen un subconjunto limitado del marco disponible para ellas y no pueden contener directivas del compilador que admitan ramas de código específicas de la plataforma.

Interfaces

El uso de interfaces permite implementar clases específicas de la plataforma que se pueden pasar a las bibliotecas compartidas para aprovechar el código común.

La interfaz se define en el código compartido y se pasa a la biblioteca compartida como parámetro o propiedad.

Después, las aplicaciones específicas de la plataforma pueden implementar la interfaz y seguir aprovechando el código compartido para "procesarlo".

Ventajas

La implementación puede contener código específico de la plataforma e incluso bibliotecas externas específicas de la plataforma de referencia.

Desventajas

Tener que crear y pasar implementaciones al código compartido. Si la interfaz se usa en profundidad dentro del código compartido, termina pasando a través de varios parámetros de método o, de lo contrario, se inserta a través de la cadena de llamadas. Si el código compartido usa muchas interfaces diferentes, todas deben crearse y establecerse en el código compartido en algún lugar.

Herencia

El código compartido podría implementar clases abstractas o virtuales que se podrían extender en uno o varios proyectos específicos de la plataforma. Esto es similar al uso de interfaces, pero con algún comportamiento ya implementado. Hay diferentes puntos de vista sobre si las interfaces o la herencia constituyen una opción mejor de diseño: en especial porque C# solo permite la herencia única que puede dictar la forma en que las API se pueden diseñar en el futuro. Use la herencia con precaución.

Las ventajas y desventajas de las interfaces son también aplicables a la herencia, con la ventaja adicional de que la clase base puede contener algún código de implementación (quizás una implementación completa independiente de la plataforma que se puede ampliar opcionalmente).

Xamarin.Forms

Consulte la documentación de Xamarin.Forms.

Otras bibliotecas multiplataforma

Estas bibliotecas también ofrecen funcionalidad multiplataforma para desarrolladores de C#:

Compilación condicional

Hay algunas situaciones en las que el código compartido seguirá funcionando de forma diferente en cada plataforma, posiblemente por el acceso a clases o características que se comportan de forma diferente. La compilación condicional funciona mejor con proyectos de recursos compartidos, donde se hace referencia al mismo archivo de código fuente en varios proyectos que tienen distintos símbolos definidos.

Los proyectos de Xamarin siempre definen __MOBILE__ como verdadero para los proyectos de aplicaciones de iOS y Android (observe el carácter de subrayado doble antes y después de estos símbolos).

#if __MOBILE__
// Xamarin iOS or Android-specific code
#endif

iOS

Xamarin.iOS define el elemento __IOS__ que puede usar para detectar dispositivos iOS.

#if __IOS__
// iOS-specific code
#endif

También hay símbolos específicos para el televisor y el reloj:

#if __TVOS__
// tv-specific stuff
#endif

#if __WATCHOS__
// watch-specific stuff
#endif

Android

El código que solo se debe compilar en aplicaciones de Xamarin.Android puede usar lo siguiente:

#if __ANDROID__
// Android-specific code
#endif

Cada versión de la API también define una nueva directiva del compilador, por lo que un código como este le permitirá agregar características si se toman como destino las API más recientes. Cada nivel de la API incluye todos los símbolos del nivel "inferior". Esta característica no es realmente útil para admitir varias plataformas; normalmente, el símbolo __ANDROID__ será suficiente.

#if __ANDROID_11__
// code that should only run on Android 3.0 Honeycomb or newer
#endif

Mac

Xamarin.Mac define el elemento __MACOS__ que puede usar para compilar solo para macOS:

#if __MACOS__
// macOS-specific code
#endif

Plataforma universal de Windows (UWP)

Use WINDOWS_UWP. No hay caracteres de subrayado alrededor de la cadena como en los símbolos de la plataforma de Xamarin.

#if WINDOWS_UWP
// UWP-specific code
#endif

Uso de la compilación condicional

Un ejemplo sencillo de caso práctico de compilación condicional consiste en establecer la ubicación del archivo de base de datos de SQLite. Las tres plataformas tienen requisitos ligeramente diferentes para especificar la ubicación del archivo:

  • iOS: Apple prefiere que los datos que no sean de usuario se coloquen en una ubicación específica (el directorio Library), pero no hay ninguna constante del sistema para este directorio. Se requiere código específico de la plataforma para compilar la ruta de acceso correcta.
  • Android: la ruta de acceso del sistema que Environment.SpecialFolder.Personal devuelve es una ubicación aceptable para almacenar el archivo de base de datos.
  • Windows Phone: el mecanismo de almacenamiento aislado no permite especificar una ruta de acceso completa, solo una ruta de acceso relativa y un nombre de archivo.
  • Plataforma universal de Windows: usa las API de Windows.Storage.

El código siguiente usa la compilación condicional para asegurarse de que el valor de DatabaseFilePath es correcto para cada plataforma:

public static string DatabaseFilePath
{
    get
    {
        var filename = "TodoDatabase.db3";
#if SILVERLIGHT
        // Windows Phone 8
        var path = filename;
#else

#if __ANDROID__
        string libraryPath = Environment.GetFolderPath(Environment.SpecialFolder.Personal);
#else
#if __IOS__
        // we need to put in /Library/ on iOS5.1 to meet Apple's iCloud terms
        // (they don't want non-user-generated data in Documents)
        string documentsPath = Environment.GetFolderPath (Environment.SpecialFolder.Personal); // Documents folder
        string libraryPath = Path.Combine (documentsPath, "..", "Library");
#else
        // UWP
        string libraryPath = Windows.Storage.ApplicationData.Current.LocalFolder.Path;
#endif
#endif
        var path = Path.Combine(libraryPath, filename);
#endif
        return path;
    }
}

El resultado es una clase que se puede compilar y usar en todas las plataformas, colocando el archivo de base de datos de SQLite en una ubicación diferente en cada plataforma.