[Xamarin] Utilizando Realm

RealmSquareIntroducción

El trabajo con datos en dispositivos móviles se ha convertido ya en algo común y habitual en el desarrollo de aplicaciones. Existe una gran variedad de tipos de datos y formas de almacenamiento:

  • Archivos de texto. Texto plano o html cacheado en el espacio de almacenamiento aislado de la aplicación.
  • Imágenes. En el espacio de almacenamiento aislado de la aplicación o almacenadas en directorios conocidos del sistema.
  • Archivos serializados. Archivos XML o Json con objetos serializados.
  • Bases de datos. Cuando se requieren datos estructurados, obtener información más compleja con consultas avanzadas entre otro tipo de necesidades, la posibilidad de las bases de datos es la elección idónea.

Realm

SQLite, engine de base datos open source es la opción más extendida y usada en el desarrollo de aplicaciones móviles. Recientemente, se ha añadido soporte a Xamarin de Realm.

Realm es una base de datos gratuita pensada para aplicaciones móviles, tabletas o wearables siendo una alternativa interesante a SQLite. Con el gran objetivo en mente de conseguir un alto rendimiento manteniendo una alta facilidad de uso, a lo largo de este artículo, crearemos una aplicación real con Xamarin.Forms y Realm donde poder crear, editar y gestionar un listado To Do.

Arrancando el proyecto

Comenzamos creando una aplicación Xamarin.Forms utilizando una librería portable (PCL):

Nueva aplicación Xamarin.Forms
Nueva aplicación Xamarin.Forms

Tras crear la aplicación, añadimos las carpetas básicas para aplicar el patrón MVVM además del paquete NuGet de Unity para la gestión del contenedor de dependencias.

Estructura del proyecto
Estructura del proyecto

Añadiendo Realm

Con el proyecto y estructura base creada, vamos a añadir Realm al proyecto. Realm esta disponible en NuGet. Vamos a añadir en cada proyecto de la solución la última versión disponible del paquete utilizando NuGet.

Hacemos clic derecho sobre la solución, Administrar paquetes NuGet para la solución…

NuGet PackagesBuscamos por la pabara “Realm” e instalamos el paquete en todos los proyectos.

Realm
Realm

La definición de modelos

En nuestra aplicación, trabajaremos con elementos del listado ToDo, una única entidad sencilla.

public class TodoItem : RealmObject
{
     public int Id { get; set; }
     public string Name { get; set; }
     public string Notes { get; set; }
     public bool Done { get; set; }
}

Los modelos en Realm son clases tradicionales con propiedades. La clase debe heredar de RealmObject para definir los modelos de datos de Realm. El trabajo con el modelo es como con cualquier modelo tradicional. Se puede añadir variables y lógica personalizada, la única restricción es que solo podremos almacenar la información de las propiedades con get y set.

A pesar de que en nuestro ejemplo no tengamos la necesidad de establecer relaciones entre múltiples modelos, podemos realizarlas de forma sumamente sencilla mediante el uso de RealmList.

En cuanto a los tipos soportados, podemos usar todos los tipos básicos (char string, int, float, double, long) además de DateTimeOffset.

La interfaz de usuario

En nuestra aplicación contaremos con dos vistas, un listado de tareas y una vista de detalles para crear, editar o eliminar una tarea específica.

Vistas
Vistas

Comenzamos definiendo la vista principal. Tendremos un listado de tareas:

<ListView 
    ItemsSource="{Binding Items}"
    SelectedItem="{Binding SelectedItem, Mode=TwoWay}">
    <ListView.ItemTemplate>
      <DataTemplate>
        <ViewCell>
          <ViewCell.View>
            <StackLayout 
              Padding="20,0,20,0"                       
              Orientation="Horizontal"       
              HorizontalOptions="FillAndExpand">
              <Label Text="{Binding Name}"
                     VerticalTextAlignment="Center"
                     HorizontalOptions="StartAndExpand" />
              <Image Source="check.png"
                     HorizontalOptions="End"
                     IsVisible="{Binding Done}"/>
            </StackLayout>
          </ViewCell.View>
        </ViewCell>
      </DataTemplate>
     </ListView.ItemTemplate>
</ListView>

A parte de definir como se visualizará cada elemento de la lista definiendo el DataTemplate establecemos la fuente de información, propiedad ItemsSource enlazada a propiedad de la ViewModel que obtendrá los datos de la base de datos.

Enlazamos la View con la ViewModel estableciendo una instancia de la ViewModel a la propiedad BindingContext de la página.

BindingContext = App.Locator.TodoItemViewModel;

En la ViewModel contaremos con una propiedad pública para definir el listado de tareas, además de la tarea seleccionada (utilizada para la navegación):

private ObservableCollection<TodoItem> _items;
private TodoItem _selectedItem;
 
public ObservableCollection<TodoItem> Items
{
     get { return _items; }
     set
     {
          _items = value;
          RaisePropertyChanged();
     }
}
 
public TodoItem SelectedItem
{
    get { return _selectedItem; }
    set
    {
          _selectedItem = value;
    }
}

Además del listado, debemos añadir en nuestra interfaz una forma de poder insertar nuevas tareas. Para ello, una de las opciones más habituales e idóneas es utilizar una Toolbar.

<ContentPage.ToolbarItems>
    <ToolbarItem Name="Add"
                 Command="{Binding AddCommand}"  >
      <ToolbarItem.Icon>
        <OnPlatform x:TypeArguments="FileImageSource"
                    Android="plus"
                    WinPhone="Assets/add.png" />
      </ToolbarItem.Icon>
    </ToolbarItem>
</ContentPage.ToolbarItems>

Añadimos un ToolbarItem que permitirá añadir elementos.

La clase Device es muy importante en Xamarin.Forms ya que nos permite acceder a una serie de propiedades y métodos con el objetivo de personalizar la aplicación según dispositivo y plataforma. Además de permitirnos detectar el tipo de dispositivo, podemos detectar la plataforma gracias a la enumeración Device.OS o personalizar elementos de la interfaz gracias al método Device.OnPlatform entre otras opciones. En nuestro ejemplo, personalizamos el icono de añadir en base a la plataforma.

Nuestra interfaz:

Vista principal
Vista principal

Añadimos elementos con un comando disponible en la ViewModel.

private ICommand _addCommand;
 
public ICommand AddCommand
{
     get { return _addCommand = _addCommand ?? new DelegateCommand(AddCommandExecute); }
}
 
private void AddCommandExecute()
{
 
}  

Al pulsar y lanzar el comando, navegaremos a la vista de detalles.

_navigationService.NavigateTo<TodoItemViewModel>(_selectedItem);

Si creamos un nuevo elemento pasaremos como parámetro una nueva entidad de TodoItem, en caso de seleccionar una existente, pasaremos el seleccionado disponible en la propiedad SelectedItem.

Definimos la interfaz de la vista de detalles:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="TodoRealm.Views.TodoItemView"
             Title="{Binding Name}">
  <StackLayout 
    VerticalOptions="StartAndExpand"
    Padding="20">
    <Label 
      Text="Name" />
    <Entry 
      Text="{Binding Name}"/>
    <Label 
      Text="Notes" />
    <Entry 
      Text="{Binding Notes}"/>
    <Label 
      Text="Done" />
    <Switch 
      x:Name="DoneEntry"
      IsToggled="{Binding Done, Mode=TwoWay}"/>
    <Button 
      Text="Save"
      Command="{Binding SaveCommand}"/>
    <Button 
      Text="Delete"
      Command="{Binding DeleteCommand}"/>
    <Button 
      Text="Cancel"
      Command="{Binding CancelCommand}"/>
  </StackLayout>
</ContentPage>

Añadimos cajas de texto para poder editar toda la información de una tarea además de botones para poder guardar, borrar o cancelar y navegar atrás.

El resultado:

Detalle
Detalle

Para enlazar la información de un elemento seleccionado, debemos capturar la información enviada en la navegación. Creamos una propiedad pública para enlazar con la UI de la tarea:

private TodoItem _item;
 
public TodoItem Item
{
     get { return _item; }
     set { _item = value; }
}

¿Cómo capturamos el elemento seleccionado en la navegación?. Utilizamos el método OnAppearing para capturar el parámetro NavigationContext.

public override void OnAppearing(object navigationContext)
{
     var todoItem = navigationContext as TodoItem;
 
     if (todoItem != null)
     {
          Item = todoItem;
     }
 
     base.OnAppearing(navigationContext);
}

En cuanto a cada botón, cada uno de ellos estará enlazado a un comando:

private ICommand _saveCommand;
private ICommand _deleteCommand;
private ICommand _cancelCommand;
 
public ICommand SaveCommand
{
     get { return _saveCommand = _saveCommand ?? new DelegateCommand(SaveCommandExecute); }
}
 
public ICommand DeleteCommand
{
     get { return _deleteCommand = _deleteCommand ?? new DelegateCommand(DeleteCommandExecute); }
}
 
public ICommand CancelCommand
{
     get { return _cancelCommand = _cancelCommand ?? new DelegateCommand(CancelCommandExecute); }
}
 
private void SaveCommandExecute()
{
 
}
 
private void DeleteCommandExecute()
{
 
}
 
private void CancelCommandExecute()
{
 
}

Trabajando con Realm

Llegados a este punto, tenemos la estructura, vistas y lógica básica necesaria de toda la aplicación. Sin embargo, aún nos falta la parte clave de la aplicación, el trabajo con Realm.

Trabajaremos con la base de datos en Realm definiendo una instancia de tipo Realm utilizando el método Realm.GetInstance(), no es más que un objeto que encapsula la base de datos.

private Realms.Realm _realm;
_realm = Realms.Realm.GetInstance();

Para hacer consultas tenemos disponible el método Realm.All<>(). Con Realm podemos utilizar LINQ para realizar consultas de la información (filtros, ordenaciones, etc.).

public IList<TodoItem> GetAll()
{
     var result = _realm.All<TodoItem>().ToList();
 
     return result;
}

De esta forma obtenemos el listado de tareas almacenadas en la base de datos para enlazar con el listado de la vista principal.

RealmObject permite auto refrescar la información, es decir, un cambio en una propiedad de un objeto tiene un efecto inmediato en cualquier otra instancia haciendo referencia.

Cualquier cambio sobre un objeto (insertar, actualizar o eliminar) debe realizarse usando una transacción

Tenemos dos formas sencillas de crear transacciones, utilizando Realm.BeginWrite() y con Realm.Write(). Ambos devueven una Transaction e implementan el patrón Dispose.

public void Insert(TodoItem item)
{
     var items = _realm.All<TodoItem>().ToList();
     var existTodoItem = items.FirstOrDefault(i => i.Id == item.Id);
             
     if (existTodoItem == null)
     {
          _realm.Write(() =>
          {
               var todoItem = _realm.CreateObject<TodoItem>();
               todoItem.Id = items.Count + 1;
               todoItem.Name = item.Name;
               todoItem.Notes = item.Notes;
               todoItem.Done = item.Done;
          });
     }
     else
     {
          using (var trans = _realm.BeginWrite())
          {
               existTodoItem.Name = item.Name;
               existTodoItem.Notes = item.Notes;
               existTodoItem.Done = item.Done;
 
               trans.Commit();
          }
     }
}

NOTA: En el caso de utilizar BeginWrite es importante tener en cuenta que debemos hacer Commit de la transacción.

Eliminar es una acción muy sencilla, basta con seleccionar el elemento a eliminar, crear una transacción y eliminar utilizando el métoto Remove.

public void Remove(TodoItem item)
{
     var items = _realm.All<TodoItem>().ToList();
     var existTodoItem = items.FirstOrDefault(i => i.Id == item.Id);
 
     if (existTodoItem != null)
     {
          using (var trans = _realm.BeginWrite())
          {
               _realm.Remove(existTodoItem);
               trans.Commit();
          }
     }
}

NOTA: También se pueden eliminar todos los objetos almacenados e Realm.

Tenéis el código fuente disponible e GitHub:

Ver GitHub

A tener en cuenta

La facilidad de uso, potencia y opciones de Realm hace que ya sea una opción interesante en el uso de datos estructurados en nuestra aplicación. Sin embargo, su nivel ed maduración es inferior al de SQLite. Contamos con algunas limitaciones como:

  • No se puede borrar en cascada.
  • Consultas asíncronas.
  • Notificaciones en colecciones.
  • Migraciones.
  • Etc.

One more thing

Exactamente el mismo ejemplo, con las mismas entidades, vistas y lógica realizo con SQLite lo tenéis también disponible en GitHub:

Ver GitHub

Recordad que podéis dejar cualquier comentario, sugerencia o duda en los comentarios.

Más información

VideoBlog, nuevo formato en el Blog!

YouTubeNovedad en el blog

Son ya varios años de vida de este modesto Blog. En todo este tiempo de forma periódica se han ido publicando artículos sobre desarrollo, quedadas, eventos y otras entradas con el objetivo de ayudar en la medida de lo posible a la comunidad. Sin duda, un formato que encaja, me divierte y sin duda seguirá. Sin embargo, en ocasiones, hay entradas que por tamaño se deben dividir en varias partes, desarrolladores interesantes que me permiten probar una App o una herramienta de desarrollo o ocasiones en las que lo que se intenta trasmitir es una experiencia más que un conocimiento puro. Para todas estas situaciones, se añadirán junto a las habituales, un nuevo formato al blog.

Nuevo formato

Para los casos anteriores, experiencias, desarrollos paso a paso que requieren mayor dedicación o momentos con otros desarrolladores para analizar nuevas herramientas o Apps nacen las “video entradas”. Serán entradas que combinarán una parte escrita pero apoyadas con la potencia y versatilidad de un video. En ocasiones serán videos grabados previamente y lanzados en conjunto con la entrada mientras que, en ocasiones de mayor interés, se realizarán videos con streaming en directo permitiendo la participación de todos por supuesto guardando la grabación para su visionado a posteriori.

Más información

[Tips and Tricks] Windows 10. Adaptar recursos por DPI de pantalla

Scale to Fit-02Introducción

Con Windows 10 los desarrolladores recibimos la culminación de un gran viaje en la convergencia entre las diferentes plataformas Windows. Ahora podemos desarrollar aplicaciones para gran diversidad de familias de dispositivos como móviles, PCs, tabletas, IoT y otros que están por llegar, compartiendo la mayor cantidad de código, con un mismo proyecto y paquete. Además, contamos con grandes nuevas características como Continuum en teléfonos que permite convertirlo en un PC utilizando Microsoft Display Dock o Miracast.

Sin embargo, hay un detalle claro y obvio. Si contamos con un mismo paquete para todos esos dispositivos diferentes…¿cómo adaptamos la experiencia para ofrecer la mejor opción adaptada posible?

Debemos adaptar la interfaz de usuario en cada familia de plataforma para lograr ofrecer la mejor experiencia posible adaptada a la perfección. Para ello, utilizamos:

  • AdaptiveTriggers
  • Nuevos controles como RelativePanel y/o SplitView
  • Detección de modos de interacción
  • Etc

Sin embargo, hay elementos vitales en la mayoría de aplicaciones, que no acaban recibiendo la atención que se merecen. Estoy hablando de las imagenes. La aplicación puede usar una imagen que se visualiza perfectamente en un teléfono pero…¿y si se usa la aplicación con Continuum en una pantalla con una resolución diferente (más elevada)?.

En este artículo, vamos a aprender como organizar y utilizar los recursos de la aplicación para que se utilicen y adapten por DPI.

DisplayInformation

La clase DisplayInformation cuenta con propiedades y eventos que nos permiten verificar y monitorear información relacionada con la pantalla física. Para monitorear detalles como cambios en DPI o la rotación podemos usar la clase DisplayInformation.

En nuestra interfaz vamos a mostrar la siguiente información:

<StackPanel
     Orientation="Horizontal">
     <TextBlock Text="Logical DPI:" />
     <TextBlock Text="{Binding LogicalDpi}" />
     <TextBlock Text="DPI" />
</StackPanel>

<StackPanel
     Orientation="Horizontal">
     <TextBlock Text="Scaling:" />
     <TextBlock Text="{Binding Scale}" />
     <TextBlock Text="%" />
</StackPanel>

De modo que, en la viewmodel bindeada definiremos dos propiedades:

private string _logicalDpi;
public string LogicalDpi
{
     get { return _logicalDpi; }
     set
     {
          _logicalDpi = value;
          RaisePropertyChanged();
     }
}

private string _scale;
public string Scale
{
     get { return _scale; }
     set
     {
          _scale = value;
          RaisePropertyChanged();
     }
}

Una para cada valor que deseamos mostrar en pantalla. Utilizaremos el método DpiChanged lanzado cada vez que la propiedad LogicalDpi se modifica, cuando cambian los píxeles por pulgada (PPI) de la pantalla.

private DisplayInformation _displayInformation;

_displayInformation = DisplayInformation.GetForCurrentView();

Tras obtener la información física actual de la pantalla utilizando el método GetForCurrentView nos suscribimos al evento DpiChanged:

_displayInformation.DpiChanged += _displayInformation_DpiChanged;

Cada vez que el evento se lanza, actualizamos la información mostrada en pantalla:

private void _displayInformation_DpiChanged(DisplayInformation sender, object args)
{
     DisplayInformation displayInformation = sender as DisplayInformation;

     UpdateDpi(displayInformation);
}

private void UpdateDpi(DisplayInformation displayInformation)
{
     if (displayInformation != null)
     {
          LogicalDpi = displayInformation.LogicalDpi.ToString();
          Scale = (displayInformation.RawPixelsPerViewPixel * 100.0).ToString();
     }
}

Mostramos los píxeles por pulgada lógica de la pantalla actual utilizando la propiedad LogicalDpi, mientras que para la escala utilizamos la propiedad RawPixelsPerViewPixel que indica el número de píxeles físicos (RAW) por cada  pixel mostrado (Layout). Para obtener la escala bastará con multiplicar el valor por cien.

NOTA: En este ejemplo utilizamos la clase DisplayInformation para mostrar información contextual relacionada con el escalado de imágenes utilizado. Sin embargo, utilizando propiedades como DiagonalSizeInInches podemos saber facilmente el tamaño en pulgadas de la pantalla y así adaptar la interfaz en consecuencia. Sumamente útil y sencillo combinado con el uso de AdaptiveTriggers personalizados.

Recursos por DPI

Para optimizar nuestra interfaz en cada posible dispositivo o condición, podemos facilitar diferentes assets para diferentes resoluciones y escalas. Cada dispositivo cuenta con una escala específica resultante de la densidad de píxeles física y la distancia de visión teórica.

La escala es utilizada por el sistema, que realiza una gestión de recursos para determinar que recurso es el más adecuado entre las opciones facilitadas por los desarrolladores en sus aplicaciones.

NOTA: Los teléfonos suelen tener una escala de entre 200 y 400 mientras que dispositivos conectados como monitores y TVs tiene valores de 100 y 150 respectivamente.

Ejemplos de escala
Ejemplos de escala

Añadiendo recursos por escala

Para soportar el uso de diferentes recursos dependientes de la escala, bastará con añadirlos de la forma adecuada. Contamos con dos formas diferentes para ello.

Por un lado, podemos sencillamente añadir el mismo recurso con diferentes tamaños utilizando la notación .scale-xxx dentro de la carpeta Assets:

Recursos por DPI
Recursos por DPI

Por otro lado, podemos añadir diferentes carpetas con el nombre de la escala, es decir, scale-xxx incluyendo como contenido los recursos.

Utilizando recursos por escala

En nuestra carpeta de Assets contamos con una imágen única llamada Picture-NoScale:

<Image
     Source="ms-appx:///Assets/Picture-NoScale.png"
     Height="100"
     Width="100"
     HorizontalAlignment="Left"/>

Con el código anterior, usaremos la misma (y única) imágen existente bajo cualquier condición. Si la escala es alta y requiere de recursos con mayor resolución, el resultado será una visualización borrosa de la misma. Proporcionamos una experiencia no idónea.

Contamos con múltiples opciones por escala del  recurso llamado Picture, bastará con utilizarlo ignorando scale-xxx de la ruta:

<Image
    Source="ms-appx:///Assets/Picture.png"
    Height="100"
    Width="100"
    HorizontalAlignment="Left"/>

Utilizando los recursos vistos previamente, donde en un caso usamos una imágen única bajo cualquier condición y en otro una adapada a diferentes escalas, el resultado es el siguiente:

Carga de Assets por DPI
Carga de Assets por DPI

Tenemos 192DPI con una escala de 200%. Podemos ver a simple vista que mientras que la primera imágen se visualiza pixelada, la segunda se aprecia con una calidad alta.

Sencillo, ¿cierto?

Tenéis el código fuente disponible e GitHub:

Ver GitHub

Recordad que podéis dejar cualquier comentario, sugerencia o duda en los comentarios.

Más información