Cómo desarrollar aplicaciones móviles con Xamarin.Forms y no morir en el intento

Xamarin.Forms

Recientemente he estado envuelto en un proyecto con Xamarin.Forms y hasta el momento ha significado retos importantes, aunque el equipo de desarrollo y yo nos encontramos en buen camino ahora. No puedo hablar mucho de la arquitectura, pero está construida alrededor del patrón MVVM.

Este articulo estará enfocado en las lecciones aprendidas a lo largo de estos meses para ayudar a los desarrolladores que, así como nosotros, se embarcan en usar esta plataforma.

No se trata de ganar mucho desempeño de una sola fuente, se trata de ganar algunos milisegundos de muchos lugares.

Jason Smith.

1. Usa el Layout correcto para cada caso

Layouts
Un layout que es capaz de mostrar múltiples hijos pero que solo tiene uno solo es un desperdicio.

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="DisplayImage.HomePage">
    <StackLayout>
        <Label Text="www.somosTechies.com" />
    </StackLayout>
</ContentPage>

Ahora bien, mucho más importante, no traten de reproducir la apariencia de un layout especifico usando una combinación de otros layouts, que como resultado solo obtendremos cálculos innecesarios realizados por los contenedores al efectuar el posicionamiento y mesuring de sus hijos, es decir al calcular los tamaños y sus posiciones.

Por ejemplo, no trates de reproducir un Grid usando una combinación de StackLayouts.

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="Details.HomePage"
             Padding="0,20,0,0">
    <ContentPage.Content>
        <StackLayout>
            <StackLayout Orientation="Horizontal">
                <Label Text="Name:" />
                <Entry Placeholder="Enter your name" />
            </StackLayout>
            <StackLayout Orientation="Horizontal">
                <Label Text="Age:" />
                <Entry Placeholder="Enter your age" />
            </StackLayout>
            <StackLayout Orientation="Horizontal">
                <Label Text="Occupation:" />
                <Entry Placeholder="Enter your occupation" />
            </StackLayout>
            <StackLayout Orientation="Horizontal">
                <Label Text="Address:" />
                <Entry Placeholder="Enter your address" />
            </StackLayout>
        </StackLayout>
    </ContentPage.Content>
</ContentPage>

Hacerlo es un completo desperdicio e impacta enormemente en el desempeño de tu aplicación. Alternativamente, usando un grid esto es mucho más eficiente.

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="Details.HomePage"
             Padding="0,20,0,0">
    <ContentPage.Content>
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="100" />
                <ColumnDefinition Width="*" />
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition Height="30" />
                <RowDefinition Height="30" />
                <RowDefinition Height="30" />
                <RowDefinition Height="30" />
            </Grid.RowDefinitions>
            <Label Text="Name:" />
            <Entry Grid.Column="1" Placeholder="Enter your name" />
            <Label Grid.Row="1" Text="Age:" />
            <Entry Grid.Row="1" Grid.Column="1" Placeholder="Enter your age" />
            <Label Grid.Row="2" Text="Occupation:" />
            <Entry Grid.Row="2" Grid.Column="1" Placeholder="Enter your occupation" />
            <Label Grid.Row="3" Text="Address:" />
            <Entry Grid.Row="3" Grid.Column="1" Placeholder="Enter your address" />
        </Grid>
    </ContentPage.Content>
</ContentPage>

Las recomendaciones de developer.xamarin.com siempre han sido buenas a la hora de diseñar nuestras aplicaciones, aquí están algunas de las más importantes:

  • Utiliza margin o padding para reducir el número de contenedores que necesitas. Mientras menos profunda sea tu jerarquía de contenedores, mejor.

  • Si usas un Grid, trata de que las filas o columnas con tamaño automático sean las menos posibles. Cada fila o columna auto-sized causa que el motor de layouut realice cálculos adicionales que son costosos y pueden costarte fluidez en tu aplicación.

  • No uses las propiedades VerticalOptions y HorizontalOptions de un layout a menos que sea requerido. Los valores por defecto (LayoutOptions.Fill y LayoutOptions.FillAndExpand ofrecen la mejor optimización).

  • Si usas un AbsoluteLayout, evita usar AutoSize mientras sea posible, esta opción es la más costosa en términos de cálculo de layout.

  • Y cuando uses un StackLayout, asegúrate que solo uno de sus hijos sea fijado con LayoutOptions.Expands, de esta forma te aseguras que ese hijo ocupe el mayor espacio que el stack pueda darle, hacer esto más de una vez es costoso e inútil.

  • Contrario a lo que piensas, los Labels son costoso, evita actualizarlos más frecuentemente de lo que necesitas, ya que actualizar un label puede significar un recalculo total de la pantalla, además del hecho de que calcular el tamaño del texto es costoso.

  • Asimismo, no uses el alineamiento vertical (VerticalTextAlignment) a menos que sea estrictamente necesario.

Absolute layout

2. Ten cuidado con los Bindings

Binding
Para los que venimos del mundo de WPF, Windows Phone, UWP,etc los bindings son familiares y sabemos que son muy poderosos, pero son costosos, así que debemos reducir los bindings innecesarios.

La forma más fácil de reducir el número de bindings es no usarlo para contenido estático que puede ser fijado fácilmente de forma explícita.

Por ejemplo, fijando Label.Text = "UserName:", tiene menos overhead que enlazar la propiedad Text a un ViewModel con la propiedad "Accept".

Binding

3. No abuses del diccionario de recursos de la aplicación.

Cualquier recurso que es usado a través de la aplicación debe ser almacenado en el diccionario de recursos de la aplicación para evitar duplicidad de código. Esto ayuda a reducir la cantidad de XAML que debe ser parseado por la aplicación.

Sin embargo, el XAML que es especifico a una página no debe ser incluido en el diccionario de recursos de la aplicación para reducir la cantidad de XAML que debe ser parseado al inicio de la aplicación y así optimizar el arranque de la misma.

4. Siempre optimiza las imágenes que uses

iOS tiene un proceso de optimización de imágenes, pero no Android, así que es mejor tener imágenes pequeñas y optimizadas tanto para optimizar el uso de la GPU como el tamaño de tu aplicación. Hay muchos servicios en línea disponibles para la optimización de imágenes pero bien podrían usar https://pnggauntlet.com/ para imágenes png y en https://imageoptim.com/versions.html pueden encontrar algunas otras herramientas.

El uso de imágenes puede incrementar rápidamente la cantidad de memoria que emplea la aplicación así que solo deben ser creadas cuando se requieran y deben ser liberadas tan pronto como no se necesiten. Cuando descargues una imagen para mostrarla, usando ImageSource.FromUri, asegurate de guardar en cache la imagen fijando la propiedad UriImageSource.CachingEnabled a true.

Xamarin images

5. Hacer profiling a la aplicación no es opcional

Xamarin Profiler nos permite monitorear el comportamiento de nuestra aplicación en búsqueda de puntos de mejora, o problemas que afecten el desempeño de nuestra aplicación móvil como uso excesivo de CPU, memory leaks, etc.

Xamarin Profiler

Puedes descargar el Profiler de Xamarin en:
https://www.xamarin.com/profiler

La información que encontramos en la herramienta de profiling de Xamarin son de 3 tipos principalmente:

  • Allocations, provee información detallada acerca de los objetos en la aplicación a medida que son creado y recolectados por el garbage collector. Podremos observer la cantidad de memoria reservada a intervalos regulares de tiempo.

  • Timing, mide exactamente cuánto tiempo lleva cada método de nuestra aplicación. La aplicación es pausada a intervalos regulares y se ejecuta un stacktrace en cada thread active. Así podremos encontrar cuellos de botella y descubrir que partes se están tardando más de lo debido.

  • Cycles, nos ayuda a encontrar objetos con ciclos de referencia que nunca podrán ser eliminados y mostrar dichos ciclos en nuestra aplicación.

Puedes revisar la documentación en:
https://developer.xamarin.com/guides/cross-platform/profiler/

6. Habilita la compilación de las vistas en XAML

XAMLC
Las vistas en XAML son una excelente forma de diseñar nuestra aplicación de forma declarativa y pueden ser compiladas directamente a IL(intermediate language) con el XAML compiler(XAMLC). Habilitar la compilación nos permite verificar en tiempo de compilación, y no de ejecución, de algún error de código. Nos permite eliminar la carga e instanciación de los elementos de XAML
asimismo, reduce el tamaño del ensamblado final ya que no necesita incluir los archivos XAML.

Para habilitar la compilación debes colocar este assembly attribute dentro de tu código.

[assembly: XamlCompilation (XamlCompilationOptions.Compile)]

Suele ser conveniente colocarlo en el archivo Properties\AssemblyInfo.cs para habilitar la compilación globalmente.

7. Entender como funciona el Garbage Collector ayuda a escribir mejor código

El garbage collector o recolector de basura se encarga de reclamar la memoria de los objetos que ya no están en uso para lenguajes administrados como C#. El GC que trae Xamarin por defecto divide el espacio de memoria para los objetos en 3 secciones:

  • La enfermería(nursery),aquí están los objetos pequeños, cuando se queda sin espacio, una recolección menor sucederá y cualquier objeto que siga vivo se moverá a la major heap.
  • Major heap, aquí es donde los objetos de más larga vida son mantenidos. Si no hay suficiente memoria, entonces una recolección ocurrirá, si no hay suficiente memoria entonces el GC solicitara más memoria al sistema.
  • Large Object Space(LOS), aquí es donde los objetos que requieren más de 8000 bytes son mantenidos, estos no inician en la enfermería sino que son colocados aquí.

El objetivo de entender el funcionamiento del garbage collector es diseñar nuestra aplicación de tal forma que reduzcamos la presión en el GC.

La recolección de memoria es un proceso importante pero debemos tener en cuenta que cuando el GC inicia una recolección, este detiene los threads de la aplicación mientras reclama la memoria, mientras tanto la aplicación puede experimentar una pequeña pausa en la UI, por eso debemos estar atentos a lo siguiente:

  • Frecuencia, que tan a menudo ocurren las recolecciones. La frecuencia incrementa a medida que más memoria es usada entre recolecciones.
  • Duración, cuánto tiempo lleva cada recolección, esto es proporcional a la cantidad de objetos vivos.

Así que para reducir la presión del GC:

  • Evita generar recolecciones al crear objetos en bucles usando pools de objetos.

  • Libera explícitamente recursos como conexiones, byte[]'s, streams, archivos tan pronto como ya no sean requeridos.

  • Desubscribete de eventos tan pronto como no sean necesarios para permitir que los objetos sean recolectados.

Revisa más en:

8. Usa cache de forma agresiva

Mobile Apps are living in a real world. Creating cross-platform apps which communicate with real-world services is easy, especially when using the Xamarin stack, leveraging C# and the .NET framework. But mobile bandwidth isn’t always and everywhere as good as it should, so a good caching strategy for the HTTP-requests the app performs to fetch data from a web service is crucial for a good user experience.

https://evolve.xamarin.com/session/56e2044afd00c0253cae33a3
Akavache

9. Usa correctamente el ListView

Es poco probable que una aplicación móvil no contenga una lista, así que usar correctamente el ListView es esencial para asegurar un buen desempeño. A continuación, listamos algunas recomendaciones:

  • NO poner un ListView dentro de un ScrollView. Considera usar las propiedades Header y Footer.

  • NO uses un TableView donde podrías usar un ListView. La única razón para usar un TableView es para UI estilo configuraciones donde cada cell tiene contenido único.

  • USA ListViewCachingStrategy.RecycleElement donde puedas.

  • USA template selectors para facilitar vistas heterogéneas dentro de un ListView. Evita sobrescribir OnBindingContextChanged para obtener el mismo efecto.

  • EVITA pasar IEnumerable como origen de datos para los ListViews. Utiliza IList en su lugar para evitar múltiples enumeraciones de la lista.

  • NO anides ListViews. En su lugar utiliza grupos dentro de un solo ListView. Anidar ListViewws esta explícitamente no soportado.

  • USA HasUnevenRows cuando tu ListView tiene filas de diferentes tamaños. Si el contenido de la celda es modificado dinámicamente asegúrate de llamar ForceUpdateSize() en la celda.

Extraído de https://evolve.xamarin.com/session/56e205b0bad314273ca4d817

10. Los Renders nativos son la mejor opción para personalizar controles y obtener el mejor performance

El poder de Xamarin.Forms reside en su abstracción de los controles específicos de cada plataforma, sin embargo, en ciertos casos necesitamos crear nuestros propios renders nativos para maximizar el desempeño de nuestra aplicación o simplemente para crear un control personalizado que no está presente en la plataforma. En el caso de desarrollar nuestros propios renders nativos debemos poner extremo cuidado al instanciar el control nativo para prevenir memory leaks.
Render
Si estas pensando en crear un control personalizado o un render nativo, deberías revisar:


En resumen

Revisa frecuentemente lo que el equipo de Xamarin tiene preparado, los issues conocidos y los workarounds asociados. Y si luego de analizar todos estos puntos y haber pasado por el foro de Xamarin, Xamarin.Forms no satisface tu escenario probablemente deberias usar Xamarin.Android y Xamarin.iOs.

¿Ya te has topado con problemas similares? Cuéntanos tus recomendaciones adicionales en los comentarios.