En Android existen diferentes formas de realizar una persistencia de datos, es decir de realizar un guardado de datos que no sea efímero y no únicamente existan durante el tiempo que la instancia de la aplicación esté vigente.

Entre estas opciones tenemos, SharedPreferences, DataStore, Ficheros, Sqlite, Room. Vemos brevemente que son cada una de ellas:

  • Los dos primeros son los que abordaremos ahora y veremos en más profundidad. Se utiliza un sistema de guardado basado en persistir valores en pares de forma key-value. Es decir guardamos un valor y le asignamos una key con la cual lo identificamos.
  • La persistencia en ficheros consiste, como su propio nombre indica, en guardar datos a un fichero y leerlos posteriormente cuando nos interese desde ese mismo  fichero.
  • Como última opción de persistencia en Android, y puede que la más usada, se encuentra el guardado de datos en una base de datos, en Android se utiliza Sqlite. Y para trabajar con la base de datos lo podemos hacer:
    •  De forma directa, es decir con clases, métodos y sentencias que directamente trabajan contra la base de datos al más puro estilo Sql.
    • O lo podemos hacer utilizando Room, un ORM, que en última instancia persiste también sobre Sqlite pero que nos permite trabajar de una forma mucho más cómoda y fácil que de la manera tradicional.

DataStore o SharedPreferences

Como decía antes, hoy toca ver un sistema de persistencia en Android que se basa en la persistencia de datos utilizando un sistema de guardado que utiliza pares key-value para guardar datos. Por ejemplo: name: “Compilación móvil”.

“Compilación móvil” sería el valor que deseamos guardar y “name”, sería la key que le asociamos al valor y mediante la cual podremos obtenerla más tarde. 

SharedPreferences utiliza la misma técnica de guardado, pero desde Google se nos dice que tiene mejor perfomance la API DataStore que la de SharedPreferences y que se utilice la primera en vez de la segunda. DataStore utiliza las coroutinas de Koltin y Flow para poder guardar el dato de forma asíncrona, de forma consistente y transaccional.

DataStore se utilizará para guardados de información que no sean muy grandes y que no sean muy complejos. Aquí no tenemos opción de hacer queries ni filtrar datos, ni gestionar los datos guardados como si de una tabla/base datos se tratase. Tampoco se debería de utilizar para guardar información sensible del usuario o de la aplicación.

Con esto en mente podemos pensar que sí que sería un buen sistema de almacenamiento para tener guardado un flag que nos permita saber si hemos mostrado con anterioridad un wizard inicial o no, si debemos mostrar un tema visual determinado al iniciar la aplicación o cosas similares. 

Preferences DataStore y Proto DataStore

La API DataStore a su vez tiene dos implementaciones diferentes: 

  • Preferences DataStore: Es la forma más simple y utiliza pares key-value para guardar la información. No necesita definir un esquema de datos previamente, puedo guardar lo que quiera, y no provee ningún tipo de consistencia de tipos a la hora de guardar información. Puedo guardar un String, un Double o un Float en la misma persistencia.
  • Proto DataStore: En este caso hay que definir el esquema con anterioridad al guardado de datos, aquí si se tienen en cuenta los tipos a guardar.  

Para usar esta API lo primero que tenemos que hacer es añadirla a nuestro proyecto y para ello solo tenemos que incluir en nuestro fichero de Gradle:

// Preferences DataStore
    implementation "androidx.datastore:datastore-preferences:1.0.0-alpha01"


    // Proto DataStore
    implementation  "androidx.datastore:datastore-core:1.0.0-alpha01"
    implementation  "com.google.protobuf:protobuf-javalite:3.10.0"

Al inicio del archivo buil.gradle de app hay que añadir:

plugins {
   id "com.google.protobuf" version "0.8.12"
}

y al final del mismo:

protobuf {
   protoc {
       artifact = "com.google.protobuf:protoc:3.10.0"
   }

   // Generates the java Protobuf-lite code for the Protobufs in this project. See
   // https://github.com/google/protobuf-gradle-plugin#customizing-protobuf-compilation
   // for more information.
   generateProtoTasks {
       all().each { task ->
           task.builtins {
               java {
                   option 'lite'
               }
           }
       }
   }
}

La versión cambiará en función de la última que esté disponible en el momento de que vosotros la utilizais.

Usando Preferences DataStore en Android

Lo primero es crear un contenedor para nuestra clave-valor que deseemos guardar y la key del valor a persistir:

private val dataStore: DataStore<Preferences> = activity.createDataStore(
        name = "settings"
    )

companion object {
        val DAY_OF_MONTH = preferencesKey<Int>("day_of_month")
    }

Y ya con el contenedor creado, vamos a guardar, por ejemplo, el número 5 con la key creada.

suspend fun saveData(value: Int) {
        dataStore.edit { mutablePreferences: MutablePreferences ->
            mutablePreferences[DAY_OF_MONTH] = value
        }
    }

Como vemos es una función suspendida, hemos dicho al principio que DataStore utiliza coroutinas y Flow para trabajar con los datos de forma asíncrona.

Y ahora que lo hemos guardado si lo deseamos recuperar:

fun fetchData(): Flow<Int> {
        return dataStore.data.map { value: Preferences ->
            value[DAY_OF_MONTH] ?: 0
        }
    }

Usando Proto DataStore en Android

Con Proto DataStore la forma de guardar datos es algo diferente ya que como veíamos antes, hay que definir un esquema de datos con anterioridad y se tiene en cuenta el tipo del dato que vayamos a guardar y que definimos en el esquema.

Lo primero es definir el esquema y para ello hay que crear un archivo en la ruta de nuestro proyecto app/src/main/proto/.

Este archivo tiene esta estructura:

syntax = "proto3";

option java_package = "com.compilacionmovil.ejemplodatastore";
option java_multiple_files = true;

message SchemeProtoDataSource {
  int32 day_of_month = 1;
}

Si bien en esta página existe toda una muy completa documentación de como definir la estructura de este archivo y el esquema de datos que deseamos guardar. No es complicado.

En nuestro caso hemos definido una estructura de datos que se llama SchemeProtoDataSource y que va a contener un valor entero de nombre day_of_month .

Una vez definido este archivo no toca crear una instancia del contenedor de datos para Proto DataStore. Antes hay que definir como se transforma el dato a guardar según el esquema definido y como ese dato se recupera, para ello hacemos lo siguiente:

object ProtoDataSourceSerializer: Serializer < SchemeProtoDataSource > {

    override fun readFrom(input: InputStream): SchemeProtoDataSource {
        try {
            return SchemeProtoDataSource.parseFrom(input)
        } catch (exception: InvalidProtocolBufferException) {
            throw CorruptionException("Cannot read proto.", exception)
        }

    }

    override fun writeTo(t: SchemeProtoDataSource, output: OutputStream) {
        t.writeTo(output)
    }
}

Esto consiste en crear una clase que implemente Serialize<T>, T será el tipo que hemos definido en nuestro archivo de esquemas, en nuestro caso nuestro tipo es SchemeProtoDataSource. La interfaz Serializer<T> nos obliga a implementar dos métodos con los cuales definimos como hay que guardar y leer nuestros datos definidos en el archivo de esquema.

Ya solo nos queda crear la instancia misma del contenedor:

private val dataStore: DataStore<SchemeProtoDataSource> = activity.createDataStore(
        fileName = "schemeProtoDataSource.proto",
        serializer = ProtoDataSourceSerializer
    )

Para guardar un dato hacemos:

suspend fun saveData(value: Int) {
        dataStore.updateData { currentSettings ->
            currentSettings.toBuilder().setDayOfMonth(value)
                .build()
        }
    }

Y para recuperar el dato:

  fun fetchData(): Flow<Int> {
        return dataStore.data
            .map { settings ->
                settings.dayOfMonth
            }
    }

¿Cuándo utilizar Preferences DataStore o Proto DataStore? Dependerá del dato a guardar. Si nuestro dato no es un simple valor y es por ejemplo un objeto complejo, pues debemos de utilizar Proto DataStore.

Si por el contrario son datos simples , valores sueltos, pues nos vendría muy bien el utilizar Preferences DataStore.

Y recordad que ambos casos de persistencia en Android el dato a guardar no debe de ser muy grande y que no podemos realizar operaciones de búsqueda, filtrado, etc… de ningún tipo en esta persistencia.

Si queréis aprender a utilizar el ORM Room de Android podéis clickar en el enlace.

Todo el código lo tenéis en Github.