[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

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *