Introducció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.
Las ventajas de utilizar una base de datos son múltiples:
- Almacenamiento estructurado con eficacia alta.
- Posibilidad de utilizar consultas y aplicar filtros.
- Posibilidad de reutilizar conocimientos de base de datos en la gestión de datos en nuestras aplicaciones móviles.
Introducción a SQLite
SQLite es un motor de base de datos Open Source utilizado en todas las plataformas móviles y adoptado tanto por Apple como Google como Microsoft. El uso de SQLite en aplicaciones móviles es una gran opción ya que:
- La base de datos es pequeña y fácil de portar.
- La base de datos se concentra en un pequeño archivo.
- Implementa la mayor parte del estándar SQL92.
Preparando el entorno
Comenzamos creando una aplicación Xamarin.Forms utilizando una librería portable (PCL):
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.
Con el proyecto y estructura base creada, vamos a añadir SQLite 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. El paquete a utilizar es SQLite.Net PCL, implementación Open Source compatible con librerías portables (PCL) con soporte a .NET y Mono.
Tras añadir la referencia vamos a crear una interfaz que defina como obtener la conexión con la base de datos y abstraer la funcionalidad específica de cada plataforma. Trabajando con SQLite, el único trabajo específico a implementar en cada plataforma es determinar la ruta a la base de datos y establecer la conexión.
public interface ISQLite { SQLiteAsyncConnection GetConnection(); }
En Android, la implementación de ISQLite nos permite establecer la conexión con la base de datos.
[assembly: Dependency(typeof(SQLiteClient))] namespace TodoSqlite.Droid.Services { public class SQLiteClient : ISQLite { public SQLiteAsyncConnection GetConnection() { var sqliteFilename = "Todo.db3"; var documentsPath = System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal); var path = Path.Combine(documentsPath, sqliteFilename); var platform = new SQLitePlatformAndroid(); var connectionWithLock = new SQLiteConnectionWithLock( platform, new SQLiteConnectionString(path, true)); var connection = new SQLiteAsyncConnection(() => connectionWithLock); return connection; } } }
NOTA: Utilizamos el atributo assembly:Dependency para poder realizar la resolución de la implementación con DependencyService.
En iOS, la implementación de ISQLite nos permite establecer la conexión con la base de datos. El archivo de la base de datos lo situamos dentro de la carpeta Library dentro del espacio de almacenamiento de la aplicación.
[assembly: Dependency(typeof(SQLiteClient))] namespace TodoSqlite.iOS.Services { public class SQLiteClient : ISQLite { public SQLiteAsyncConnection GetConnection() { var sqliteFilename = "Todo.db3"; var documentsPath = Environment.GetFolderPath(Environment.SpecialFolder.Personal); var libraryPath = Path.Combine(documentsPath, "..", "Library"); var path = Path.Combine(libraryPath, sqliteFilename); var platform = new SQLitePlatformIOS(); var connectionWithLock = new SQLiteConnectionWithLock( platform, new SQLiteConnectionString(path, true)); var connection = new SQLiteAsyncConnection(() => connectionWithLock); return connection; } } }
Todo listo para comenzar!
La definición de modelos
En nuestra aplicación, trabajaremos con elementos del listado ToDo, una única entidad sencilla.
public class TodoItem { [PrimaryKey, AutoIncrement] public int Id { get; set; } public string Name { get; set; } public string Notes { get; set; } public bool Done { get; set; } }
La gestión de campos especiales o relacionados las gestionamos mediante el uso de etiquetas. En nuestro ejemplo establecemos el campo Id como clave primaria gracias a la etiqueta PrimaryKey y además que autoincremente con el uso de AutoIncrement.
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.
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.
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:
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; } }
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:
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 SQLite
Para trabajar con la base de datos utilizaremos DependencyService para obtener la implementación de ISQLite y obtener una conexión.
private SQLiteAsyncConnection _sqlCon; _sqlCon = DependencyService.Get<ISQLite>().GetConnection();
Para almacenar nuestras tareas, comenzamos creando la tabla necesaria en la base de datos.
public async void CreateDatabaseAsync() { using (await Mutex.LockAsync().ConfigureAwait(false)) { await _sqlCon.CreateTableAsync<TodoItem>().ConfigureAwait(false); } }
Utilizamos el método CreateTableAsync<>() para crear la tabla.
Continuamos con las operaciones básicas de CRUD. Para obtener la información almacenada en una tabla podemos acceder a la tabla y obtener el listado utilizando el método ToListAsync.
public async Task<IList<TodoItem>> GetAll() { var items = new List<TodoItem>(); using (await Mutex.LockAsync().ConfigureAwait(false)) { items = await _sqlCon.Table<TodoItem>().ToListAsync().ConfigureAwait(false); } return items; }
NOTA: Podemos realizar consultar SQL utilizando el método QueryAync.
A la hora de insertar, verificamos si estamos ante un registro existente o no, para realizar el registro de un nuevo elemento o actualizar uno existente con los métodos InsertAsync o UpdateAsync respectivamente.
public async Task Insert(TodoItem item) { using (await Mutex.LockAsync().ConfigureAwait(false)) { var existingTodoItem = await _sqlCon.Table<TodoItem>() .Where(x => x.Id == item.Id) .FirstOrDefaultAsync(); if (existingTodoItem == null) { await _sqlCon.InsertAsync(item).ConfigureAwait(false); } else { item.Id = existingTodoItem.Id; await _sqlCon.UpdateAsync(item).ConfigureAwait(false); } } }
Eliminar es una acción sencilla realizada con el método DeleteAsync.
public async Task Remove(TodoItem item) { await _sqlCon.DeleteAsync(item); }
Tenéis el código fuente disponible e GitHub:
Recordad que podéis dejar cualquier comentario, sugerencia o duda en los comentarios.
Más información
- Xamarin: Cross-Platform Data Access
- Xamarin: Working with a Local Database