DataStore   Parte de Android Jetpack

Jetpack DataStore es una solución de almacenamiento de datos que te permite almacenar pares clave-valor, o bien objetos escritos con búferes de protocolo. Datastore usa corrutinas y Flow de Kotlin para almacenar datos de manera asíncrona, coherente y transaccional.

Si actualmente usas SharedPreferences para almacenar datos, considera migrar a Datastore.

Preferences Datastore y Proto Datastore

Datastore ofrece dos implementaciones diferentes: Preferences Datastore y Proto Datastore.

  • Preferences Datastore almacena y accede a datos mediante claves. Esta implementación no requiere un esquema predefinido ni proporciona seguridad de tipos.
  • Proto Datastore almacena datos como instancias de un tipo personalizado de datos. Esta implementación requiere que definas un esquema con búferes de protocolo, pero proporciona seguridad de tipos.

Cómo usar DataStore correctamente

Ten siempre en cuenta las siguientes reglas para usar DataStore de forma correcta:

  1. Nunca crees más de una instancia de DataStore para un archivo determinado en el mismo proceso. Esta acción puede dañar toda la funcionalidad de DataStore. Si hay varios servicios de DataStore activos para un archivo determinado en el mismo proceso, DataStore lanzará IllegalStateException cuando lea o actualice datos.

  2. El tipo genérico de DataStore debe ser inmutable. La mutación de uno de los tipos usados hará que todas las garantías que DataStore proporcione sean no válidas y creará errores potencialmente graves y difíciles de detectar. Se recomienda el uso de búferes de protocolo que proporcionen garantías de inmutabilidad, una API simple y una serialización eficiente.

  3. Nunca mezcles usos de SingleProcessDataStore y MultiProcessDataStore para el mismo archivo. Si deseas acceder a DataStore desde más de un proceso, usa siempre MultiProcessDataStore.

Configuración

Para usar Jetpack Datastore en tu app, agrega lo siguiente a tu archivo de Gradle según la implementación que quieras usar:

Preferences DataStore

Groovy

    // Preferences DataStore (SharedPreferences like APIs)
    dependencies {
        implementation "androidx.datastore:datastore-preferences:1.1.1"

        // optional - RxJava2 support
        implementation "androidx.datastore:datastore-preferences-rxjava2:1.1.1"

        // optional - RxJava3 support
        implementation "androidx.datastore:datastore-preferences-rxjava3:1.1.1"
    }

    // Alternatively - use the following artifact without an Android dependency.
    dependencies {
        implementation "androidx.datastore:datastore-preferences-core:1.1.1"
    }
    

Kotlin

    // Preferences DataStore (SharedPreferences like APIs)
    dependencies {
        implementation("androidx.datastore:datastore-preferences:1.1.1")

        // optional - RxJava2 support
        implementation("androidx.datastore:datastore-preferences-rxjava2:1.1.1")

        // optional - RxJava3 support
        implementation("androidx.datastore:datastore-preferences-rxjava3:1.1.1")
    }

    // Alternatively - use the following artifact without an Android dependency.
    dependencies {
        implementation("androidx.datastore:datastore-preferences-core:1.1.1")
    }
    

Proto DataStore

Groovy

    // Typed DataStore (Typed API surface, such as Proto)
    dependencies {
        implementation "androidx.datastore:datastore:1.1.1"

        // optional - RxJava2 support
        implementation "androidx.datastore:datastore-rxjava2:1.1.1"

        // optional - RxJava3 support
        implementation "androidx.datastore:datastore-rxjava3:1.1.1"
    }

    // Alternatively - use the following artifact without an Android dependency.
    dependencies {
        implementation "androidx.datastore:datastore-core:1.1.1"
    }
    

Kotlin

    // Typed DataStore (Typed API surface, such as Proto)
    dependencies {
        implementation("androidx.datastore:datastore:1.1.1")

        // optional - RxJava2 support
        implementation("androidx.datastore:datastore-rxjava2:1.1.1")

        // optional - RxJava3 support
        implementation("androidx.datastore:datastore-rxjava3:1.1.1")
    }

    // Alternatively - use the following artifact without an Android dependency.
    dependencies {
        implementation("androidx.datastore:datastore-core:1.1.1")
    }
    

Almacena pares clave-valor con Preferences Datastore

La implementación de Preferences Datastore usa las clases DataStore y Preferences para conservar pares clave-valor simples en el disco.

Crea un Preferences Datastore

Usa el delegado de propiedad que creó preferencesDataStore para crear una instancia de DataStore<Preferences>. Llámalo una vez en el nivel superior de tu archivo de Kotlin y accede a él a través de esta propiedad en el resto de tu aplicación. Esto hace que sea más fácil mantener tu DataStore como un singleton. De manera alternativa, usa RxPreferenceDataStoreBuilder si utilizas RxJava. El parámetro name obligatorio es el nombre de Preferences Datastore.

Kotlin

// At the top level of your kotlin file:
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")

Java

RxDataStore<Preferences> dataStore =
  new RxPreferenceDataStoreBuilder(context, /*name=*/ "settings").build();

Lee desde Preferences Datastore

Debido a que Preferences Datastore no usa un esquema predefinido, debes usar la función de tipo de clave correspondiente para definir una clave para cada valor que necesites almacenar en la instancia DataStore<Preferences>. Por ejemplo, si deseas definir una clave para un valor int, usa intPreferencesKey(). Luego, usa la propiedad DataStore.data para exponer el valor almacenado adecuado mediante un Flow.

Kotlin

val EXAMPLE_COUNTER = intPreferencesKey("example_counter")
val exampleCounterFlow: Flow<Int> = context.dataStore.data
  .map { preferences ->
    // No type safety.
    preferences[EXAMPLE_COUNTER] ?: 0
}

Java

Preferences.Key<Integer> EXAMPLE_COUNTER = PreferencesKeys.int("example_counter");

Flowable<Integer> exampleCounterFlow =
  dataStore.data().map(prefs -> prefs.get(EXAMPLE_COUNTER));

Escribe en un Preferences Datastore

Preferences Datastore proporciona una función edit() que actualiza los datos de forma transaccional en un DataStore. El parámetro transform de la función acepta un bloque de código en el que puedes actualizar los valores según sea necesario. Todo el código que esté en el bloque de transformación se tratará como una sola transacción.

Kotlin

suspend fun incrementCounter() {
  context.dataStore.edit { settings ->
    val currentCounterValue = settings[EXAMPLE_COUNTER] ?: 0
    settings[EXAMPLE_COUNTER] = currentCounterValue + 1
  }
}

Java

Single<Preferences> updateResult =  dataStore.updateDataAsync(prefsIn -> {
  MutablePreferences mutablePreferences = prefsIn.toMutablePreferences();
  Integer currentInt = prefsIn.get(INTEGER_KEY);
  mutablePreferences.set(INTEGER_KEY, currentInt != null ? currentInt + 1 : 1);
  return Single.just(mutablePreferences);
});
// The update is completed once updateResult is completed.

Almacena objetos escritos con Proto Datastore

La implementación de Proto Datastore usa Datastore y búferes de protocolo para conservar los objetos escritos en el disco.

Define un esquema

Proto Datastore requiere un esquema predefinido en un archivo proto del directorio app/src/main/proto/. Este esquema define el tipo para los objetos que conserves en tu Proto Datastore. Si quieres obtener más información para definir un esquema proto, consulta la guía del lenguaje de protobuf.

syntax = "proto3";

option java_package = "com.example.application";
option java_multiple_files = true;

message Settings {
  int32 example_counter = 1;
}

Cómo crear un Proto Datastore

Hay dos pasos relacionados con la creación de un Proto Datastore para almacenar tus objetos escritos:

  1. Define una clase que implemente Serializer<T>, en la que T es el tipo definido en el archivo proto. Esta clase de serializador le indica a Datastore cómo leer y escribir el tipo de datos. Asegúrate de incluir un valor predeterminado para que el serializador se use si aún no se creó un archivo.
  2. Usa el delegado de propiedad que creó dataStore para crear una instancia de DataStore<T>, en la que T es el tipo definido en el archivo .proto. Llámalo una vez en el nivel superior de tu archivo de Kotlin y accede a él mediante este delegado de propiedad en el resto de tu app. El parámetro filename le indica a Datastore qué archivo debe usar para almacenar los datos, y el parámetro serializer le indica el nombre de la clase de serializador definida en el paso 1.

Kotlin

object SettingsSerializer : Serializer<Settings> {
  override val defaultValue: Settings = Settings.getDefaultInstance()

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

  override suspend fun writeTo(
    t: Settings,
    output: OutputStream) = t.writeTo(output)
}

val Context.settingsDataStore: DataStore<Settings> by dataStore(
  fileName = "settings.pb",
  serializer = SettingsSerializer
)

Java

private static class SettingsSerializer implements Serializer<Settings> {
  @Override
  public Settings getDefaultValue() {
    Settings.getDefaultInstance();
  }

  @Override
  public Settings readFrom(@NotNull InputStream input) {
    try {
      return Settings.parseFrom(input);
    } catch (exception: InvalidProtocolBufferException) {
      throw CorruptionException(“Cannot read proto.”, exception);
    }
  }

  @Override
  public void writeTo(Settings t, @NotNull OutputStream output) {
    t.writeTo(output);
  }
}

RxDataStore<Byte> dataStore =
    new RxDataStoreBuilder<Byte>(context, /* fileName= */ "settings.pb", new SettingsSerializer()).build();

Lee desde un Proto Datastore

Usa DataStore.data para exponer un Flow de la propiedad correspondiente desde tu objeto almacenado.

Kotlin

val exampleCounterFlow: Flow<Int> = context.settingsDataStore.data
  .map { settings ->
    // The exampleCounter property is generated from the proto schema.
    settings.exampleCounter
  }

Java

Flowable<Integer> exampleCounterFlow =
  dataStore.data().map(settings -> settings.getExampleCounter());

Escribe en un Proto Datastore

Proto Datastore proporciona una función updateData() que actualiza un objeto almacenado de forma transaccional. updateData() te muestra el estado actual de los datos como una instancia de tu tipo de datos y los actualiza de forma transaccional en una operación atómica de lectura, escritura y modificación.

Kotlin

suspend fun incrementCounter() {
  context.settingsDataStore.updateData { currentSettings ->
    currentSettings.toBuilder()
      .setExampleCounter(currentSettings.exampleCounter + 1)
      .build()
    }
}

Java

Single<Settings> updateResult =
  dataStore.updateDataAsync(currentSettings ->
    Single.just(
      currentSettings.toBuilder()
        .setExampleCounter(currentSettings.getExampleCounter() + 1)
        .build()));

Usa Datastore en código síncrono

Uno de los beneficios principales de Datastore es la API asíncrona, pero quizás no siempre se pueda cambiar el código que lo rodea para que sea asíncrono. Este podría ser el caso si trabajas con una base de código existente que usa operaciones síncronas de E/S del disco o si tienes una dependencia que no proporciona una API asíncrona.

Las corrutinas de Kotlin proporcionan el compilador de corrutinas runBlocking() para ayudar a cerrar la brecha entre el código síncrono y el asíncrono. Puedes usar runBlocking() para leer datos de Datastore de forma síncrona. RxJava ofrece métodos de bloqueo en Flowable. El siguiente código bloquea el subproceso de llamada hasta que Datastore muestre datos:

Kotlin

val exampleData = runBlocking { context.dataStore.data.first() }

Java

Settings settings = dataStore.data().blockingFirst();

Realizar operaciones de E/S síncronas en el subproceso de IU puede provocar ANR o bloqueos de IU. Para mitigar estos problemas, puedes precargar los datos de Datastore de forma asíncrona:

Kotlin

override fun onCreate(savedInstanceState: Bundle?) {
    lifecycleScope.launch {
        context.dataStore.data.first()
        // You should also handle IOExceptions here.
    }
}

Java

dataStore.data().first().subscribe();

De esta manera, Datastore lee de modo asíncrono los datos y los almacena en caché, en la memoria. Las lecturas síncronas posteriores con runBlocking() pueden ser más rápidas o evitar por completo una operación de E/S del disco si se completó la lectura inicial.

Cómo usar DataStore en código de varios procesos

Puedes configurar DataStore para acceder a los mismos datos en diferentes procesos con las mismas garantías de coherencia de datos que brinda un solo proceso. Esto garantizará, en particular, lo siguiente:

  • Las lecturas solo mostrarán los datos conservados en el disco.
  • Habrá coherencia de la lectura después de escritura.
  • Habrá una serialización en las escrituras.
  • Las escrituras nunca bloquearán lecturas.

Piensa en una aplicación de muestra con un servicio y una actividad:

  1. El servicio se ejecuta en un proceso separado y actualiza el almacén de datos de forma periódica.

    <service
      android:name=".MyService"
      android:process=":my_process_id" />
    
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
          scope.launch {
              while(isActive) {
                  dataStore.updateData {
                      Settings(lastUpdate = System.currentTimeMillis())
                  }
                  delay(1000)
              }
          }
    }
    
  2. Por su parte, es posible que la app recopile esos cambios y actualice la IU

    val settings: Settings by dataStore.data.collectAsState()
    Text(
      text = "Last updated: $${settings.timestamp}",
    )
    

Para usar DataStore en diferentes procesos, debes construir el objeto DataStore con MultiProcessDataStoreFactory.

val dataStore: DataStore<Settings> = MultiProcessDataStoreFactory.create(
   serializer = SettingsSerializer(),
   produceFile = {
       File("${context.cacheDir.path}/myapp.preferences_pb")
   }
)

serializer le indica a DataStore cómo leer y escribir tu tipo de datos. Asegúrate de incluir un valor predeterminado para usar el serializador si aún no se creó un archivo. A continuación, se muestra un ejemplo de implementación con kotlinx.serialization:

@Serializable
data class Settings(
   val lastUpdate: Long
)

@Singleton
class SettingsSerializer @Inject constructor() : Serializer<Settings> {

   override val defaultValue = Settings(lastUpdate = 0)

   override suspend fun readFrom(input: InputStream): Timer =
       try {
           Json.decodeFromString(
               Settings.serializer(), input.readBytes().decodeToString()
           )
       } catch (serialization: SerializationException) {
           throw CorruptionException("Unable to read Settings", serialization)
       }

   override suspend fun writeTo(t: Settings, output: OutputStream) {
       output.write(
           Json.encodeToString(Settings.serializer(), t)
               .encodeToByteArray()
       )
   }
}

Puedes usar la inserción de dependencia Hilt para asegurarte de tener una única instancia de DataStore por proceso:

@Provides
@Singleton
fun provideDataStore(@ApplicationContext context: Context): DataStore<Settings> =
   MultiProcessDataStoreFactory.create(...)

Cómo controlar la corrupción de archivos

En ocasiones excepcionales, el archivo persistente en el disco de DataStore podría dañarse. De forma predeterminada, DataStore no se recupera automáticamente de la corrupción, y los intentos de leerlo harán que el sistema arroje una CorruptionException.

DataStore ofrece una API de controlador de corrupción que puede ayudarte a recuperarte de forma fluida en una situación de este tipo y evitar que se arroje la excepción. Cuando se configura, el controlador de daños reemplaza el archivo dañado por uno nuevo que contiene un valor predeterminado predefinido.

Para configurar este controlador, proporciona un corruptionHandler cuando crees la instancia de DataStore en by dataStore() o en el método de fábrica DataStoreFactory:

val dataStore: DataStore<Settings> = DataStoreFactory.create(
   serializer = SettingsSerializer(),
   produceFile = {
       File("${context.cacheDir.path}/myapp.preferences_pb")
   },
   corruptionHandler = ReplaceFileCorruptionHandler { Settings(lastUpdate = 0) }
)

Envía comentarios

Usa estos recursos para compartir tus comentarios y tus ideas con nosotros:

Herramienta de seguimiento de errores
Informa los problemas para que podamos corregir los errores.

Recursos adicionales

Para obtener más información sobre Jetpack Datastore, consulta los siguientes recursos adicionales:

Ejemplos

Blogs

Codelabs