Usando SQLite en desarrollos multiplataforma

Imaginemos que estamos desarrollando una App Universal (para Windows 8.1 y Windows Phone 8.1) y/o una app de Xamarin o Xamarin.Forms (para Windows Phone 8, Android e iOS). Una de las cosas que queremos hacer en nuestra app es almacenar datos en una base de datos local SQLite. Además, queremos compartir todo el código posible entre todas estas plataformas, por lo que creamos una PCL (Portable Class Library) que referenciamos en todos nuestros proyectos de plataforma (como ya vimos que podíamos hacer en el artículo Librerías Portables: Comparte código entre múltiples plataformas).

image

Ahora, para trabajar con SQLite en este escenario, ¿cómo hemos de configurar cada proyecto? ¿Qué código podemos reutilizar en la PCL y qué código tenemos que poner en cada uno de los proyectos de plataforma?

 

Empezaremos viendo la configuración necesaria para cada proyecto, y para ello nos apoyaremos en un pequeño ejemplo que he creado y que está compuesto por los siguientes proyectos:

  • Common
    • SQLiteDemo.Portable: librería portable que soporta todas las plataformas mostradas en la imagen anterior.
  • Universal
    • SQLiteDemo.Windows: proyecto de plataforma Windows 8.1.
    • SQLiteDemo.WindowsPhone: proyecto de plataforma Windows Phone 8.1.
    • SQLiteDemo.Shared: proyecto con el código compartido entre SQLiteDemo.Windows y SQLiteDemo.WindowsPhone.
  • Xamarin.Forms
    • SQLiteDemo.XamarinForms.WindowsPhone: proyecto de Xamarin.Forms para plataforma Windows Phone 8.
    • SQLiteDemo.XamarinForms.Droid: proyecto de Xamarin.Forms para plataforma Android.
    • SQLiteDemo.Shared: proyecto con el código compartido entre SQLiteDemo.XamarinForms.Droid y SQLiteDemo.XamarinForms.WindowsPhone.

 

Aunque en el ejemplo haya usado Xamarin.Forms, si hubiese utilizado Xamarin clásico todo lo que comente debería de funcionar igual. Además podrás notar que no he incluido el proyecto SQLiteDemo.XamarinForms.iOS de Xamarin.Forms para la plataforma iOS en el ejemplo, ya que no tengo un Mac en el que poder compilarlo para hacer pruebas. Aún así mencionaré que podríamos hacer en esta plataforma por si te es de utilidad.

 

Configuración de SQLiteDemo.Portable

Para poder trabajar con SQLite he utilizado SQLite.Net, un conocido cliente de SQLite multiplataforma para .NET cuyo código podemos encontrar en GitHub: SQLite.Net-PCL. SQLite.Net nos abstraerá de la implementación particular de SQLite en cada plataforma.

En esta ocasión he optado por usar la versión asíncrona de SQLite.NET, por lo que tenemos que referenciar en este proyecto el paquete NuGet SQLite.Net.Async PCL, que automáticamente incluirá el paquete SQLite.Net PCL. Si quisiésemos usar la versión síncrona, sería suficiente con referenciar SQLite.Net PCL.

image

 

Configuración de SQLiteDemo.Windows

Antes de empezar tenemos que instalar la extensión de SQLite for Windows Runtime (Windows 8.1) en nuestro Visual Studio. Con ella tendremos los componentes necesarios para poder usar SQLite en nuestras apps de Windows 8.1. Una vez instalada la extensión la referenciamos en nuestro proyecto. Como los componentes de SQLite para Windows 8.1 están hechos en C++, al referenciarlos también se añadirá automáticamente una referencia el runtime de C++ correspondiente:

image

Después ya podemos referenciar el paquete NuGet SQLite.Net PCL - WinRT Platform, que automáticamente incluirá el paquete SQLite.Net PCL, y que nos permitirá utilizar SQLite desde .NET en apps de Windows 8.1.

image

Es importante recordar que al referenciar código C++ en nuestro proyecto tendremos que compilarlo para x86 o ARM en función del dispositivo donde queramos ejecutar nuestra app. Compilar para “Any CPU” no funcionará.

 

Configuración de SQLiteDemo.WindowsPhone

Al igual que en Windows 8.1, antes de empezar tenemos que instalar la extensión de SQLite for Windows Phone 8.1 y referenciarla en nuestro proyecto. A partir de ahí no podremos compilar como “Any CPU” y tendremos que compilar para x86 (para probar en el emulador) o para ARM (para ejecutar en un teléfono).

image

Ahora, a diferencia de Windows 8.1, no tenemos un paquete NuGet de SQLite.NET específico para Windows Phone 8.1, y el de Windows 8.1 no nos deja referenciarlo en el proyecto. De hecho, si vamos al GitHub de SQlite.NET-PCL vemos que hay un proyecto SQLite.Net.Platform.WindowsPhoneApp81 pero está vacío.

Por fortuna hay una solución alternativa mientras no exista el paquete NuGet adecuado: podemos descargarnos de GitHub el código del proyecto de SQLite.Net.Platform.WinRT para Windows 8.1, en las propiedades le cambiamos el target a Windows Phone 8.1, y lo referenciamos a nuestro proyecto. Compilará y funcionará sin cambios tras añadirle una referencia la paquete SQLite.Net PCL. También tendremos que referenciar el paquete SQLite.Net PCL en el proyecto de Windows Phone 8.1. Y por fin podremos utilizar SQLite desde .NET en apps de Windows Phone 8.1.

 

Configuración de SQLiteDemo.XamarinForms.WindowsPhone

Al igual que en los casos anteriores, necesitamos instalar la extensión SQLite for Windows Phone y referenciarla en nuestro proyecto. Una vez más, la implementación de SQLite para esta plataforma está hecha en C++, por lo que no podremos compilar para “Any CPU”.

image

Después ya podemos referenciar el paquete NuGet SQLite.Net PCL - WindowsPhone8 Platform, que automáticamente incluirá el paquete SQLite.Net PCL, y que nos permitirá utilizar SQLite desde .NET en apps de Windows Phone 8.0 (ya usen o no Xamarin.Forms).

image

 

Configuración de SQLiteDemo.XamarinForms.Droid

Para poder utilizar SQLite desde .NET en apps de Android hechas con Xamarin o con Xamarin.Forms, es suficiente con referenciar el paquete SQLite.Net PCL - XamarinAndroid Platform.

image

 

Configuración de SQLiteDemo.XamarinForms.iOS (si lo hubiera en el ejemplo)

Para poder utilizar SQLite desde .NET en apps de iOS hechas con Xamarin o con Xamarin.Forms, en principio sería suficiente con referenciar el paquete SQLite.Net PCL - XamarinIOS Platform, aunque no estoy seguro de que funcione con la nueva iOS Unified API. Ahora, he podido ver en GitHub que hay un nuevo proyecto para Xamarin.iOS Unified API que podríamos descargarnos y utilizar en caso de no servirnos el paquete NuGet: SQLite.Net.Platform.XamarinIOS.Unified.

 

Pasemos ahora a hablar del código, qué podemos reutilizar y qué no.

 

Código compartido en SQLiteDemo.Portable

En la PCL podemos incluir las clases de las entidades que almacenaremos en nuestra base de datos SQLite, como por ejemplo:

 public class Person
 {
     [PrimaryKey, AutoIncrement]
     public int Id { get; set; }
  
     public string Name { get; set; }
  
     public string Description { get; set; }
 }

También podemos incluir los repositorios que usaremos para trabajar con las tablas de SQLite para esas entidades, como por ejemplo:

 public class PersonRepository : BaseSQLiteRepository<Person>
 {
     public PersonRepository(ISQLiteDataContext dataContext)
         : base(dataContext)
     {
     }
 }

Y podemos incluir clases como BaseSQLiteRepository, que sería la clase base de los repositorios de este ejemplo:

 public class BaseSQLiteRepository<T> : IRepository<T>
     where T : class, new()
 {
     protected SQLiteAsyncConnection Connection { get; set; }
  
     public BaseSQLiteRepository(ISQLiteDataContext dataContext)
     {
         Connection = dataContext.SQLiteAsyncConnection;
     }
  
     public Task InitializeAsync()
     {
         return Connection.CreateTableAsync<T>();
     }
  
     public async Task<T> CreateAsync(T value)
     {
         await Connection.InsertAsync(value);
         return value;
     }
  
     public Task<int> CreateAsync(IEnumerable values)
     {
         return Connection.InsertAllAsync(values);
     }
  
     public Task<List<T>> FindAsync(Expression<Func<T, bool>> predicate)
     {
         return Connection.Table<T>().Where(predicate).ToListAsync();
     }
  
     public Task<int> UpdateAsync(T value)
     {
         return Connection.UpdateAsync(value);
     }
  
     public Task<int> DeleteAsync(T value)
     {
         return Connection.DeleteAsync(value);
     }
  
     public async Task<int> DeleteAsync(Expression<Func<T, bool>> predicate)
     {
         List<T> objects = await FindAsync(predicate);
         foreach (T oneObject in objects)
         {
             await DeleteAsync(oneObject);
         }
  
         return objects.Count;
     }
 }

Y podemos incluir en la PCL el interfaz que tienen que implementar dichos repositorios, IRepository:

 public interface IRepository<T>
     where T : class, new()
 {
     Task<T> CreateAsync(T value);
  
     Task<List<T>> FindAsync(Expression<Func<T, bool>> predicate);
  
     Task<int> UpdateAsync(T value);
  
     Task<int> DeleteAsync(T value);
 }

También podemos incluir por ejemplo una clase factoría que nos cree los repositorios y nos los inicialice:

 public class RepositoryFactory
 {
     private ISQLiteDataContext DataContext { get; set; }
  
     public RepositoryFactory(SQLiteDataContext dataContext)
     {
         DataContext = dataContext;
     }
  
     public PersonRepository Persons
     {
         get
         {
             var repository = new PersonRepository(DataContext);
             repository.InitializeAsync().Wait();
             return repository;
         }
     }
 }

Y por supuesto en la PCL podemos tener todo el código de nuestras View Model, que serán las que accedan a los repositorios para por ejemplo escribir o leer en la base de datos SQLite de manera muy sencilla:

 await Repositories.Persons.CreateAsync(
     new Person() { Name = Name, Description = Description });
  
 ...
  
 Persons = await Repositories.Persons.FindAsync(
     person => person.Name.ToLower().Contains(nameContains));

En este ejemplo los repositorios necesitan un contexto de datos que implemente el interfaz ISQLiteDataContext. Este contexto proporcionará a los repositorios la conexión a la base de datos SQLite que necesitan. El código de este interfaz también lo podemos incluir en la PCL:

 public interface ISQLiteDataContext
 {
     SQLiteAsyncConnection SQLiteAsyncConnection { get; }
 }

Ahora, para crear la conexión a la base de datos SQLite proporcionada por el contexto de datos, necesitamos la implementación particular de SQLite (ISQLitePlatform) y la cadena de conexión de la plataforma en la que estemos (SQLiteConnectionString). Esos datos los desconocemos en la PCL, pero aún así podremos crear en ella una clase común que cree la conexión con la información que obtenga de cada una de las plataformas donde se use la PCL:

 public abstract class SQLiteDataContext : ISQLiteDataContext
 {
     protected string DatabaseName { get { return "test.db"; } }
  
     protected abstract ISQLitePlatform SQLitePlatform { get; }
  
     protected abstract SQLiteConnectionString ConnectionString { get; }
  
     public SQLiteAsyncConnection SQLiteAsyncConnection { get; private set; }
  
     public SQLiteDataContext()
     {
         SQLiteAsyncConnection = new SQLiteAsyncConnection(
             () => new SQLiteConnectionWithLock(SQLitePlatform, ConnectionString));
     }
 }

Luego en cada proyecto particular implementaremos el contexto de datos con la información que la PCL desconoce. El contexto de datos particular de cada plataforma será el que tengamos que proporcionarle a los repositorios mediante algún mecanismo de inyección de dependencias.

 

Código particular en SQLiteDemo.Shared

Para Windows 8.1 y Windows Phone 8.1 la implementación particular del contexto de datos es la misma, por lo que podemos compartirla, y sería así:

 public class SQLiteDataContextWinRT : SQLiteDataContext
 {
     protected override ISQLitePlatform SQLitePlatform
     {
         get
         {
             return new SQLitePlatformWinRT();
         }
     }
  
     protected override SQLiteConnectionString ConnectionString
     {
         get
         {
             return new SQLiteConnectionString(
                 Path.Combine(ApplicationData.Current.LocalFolder.Path, DatabaseName), false);
         }
     }
 }

 

Código particular en SQLiteDemo.XamarinForms.WindowsPhone

Para Windows Phone 8 la implementación particular del contexto de datos sería así:

 public class SQLiteDataContextWP8 : SQLiteDataContext
 {
     protected override ISQLitePlatform SQLitePlatform
     {
         get
         {
             return new SQLitePlatformWP8();
         }
     }
  
     protected override SQLiteConnectionString ConnectionString
     {
         get
         {
             return new SQLiteConnectionString(Path.Combine(ApplicationData.Current.LocalFolder.Path, DatabaseName), false);
         }
     }
 }
  

Código particular en SQLiteDemo.XamarinForms.Droid

Para Android la implementación particular del contexto de datos sería así:

 public class SQLiteDataContextAndroid : SQLiteDataContext
 {
     protected override ISQLitePlatform SQLitePlatform
     {
         get
         {
             return new SQLitePlatformAndroid();
         }
     }
  
     protected override SQLiteConnectionString ConnectionString
     {
         get
         {
             return new SQLiteConnectionString(
                 Path.Combine(Forms.Context.ApplicationContext.FilesDir.AbsolutePath, DatabaseName), false);
         }
     }
 }

 

Y para iOS sería similar.

 

En este proyecto de GitHub puedes encontrar todo el código de este ejemplo.

 

Un saludo,

Alejandro Campos Magencio (@alejacma)

Technical Evangelist

PD: Mantente informado de todas las novedades de Microsoft para los desarrolladores españoles a través del Twitter de MSDN, el Facebook de MSDN, el Blog de MSDN y la Newsletter MSDN Flash.