Arquitectura de apps: Capa de datos - DataStore - Android Developers

Project: /architecture/_project.yaml Book: /architecture/_book.yaml keywords: datastore, architecture, api:JetpackDataStore description: Explore this app architecture guide on data layer libraries to learn about Preferences DataStore and Proto DataStore, Setup, and more. hide_page_heading: true

DataStore   Parte de Android Jetpack

Prueba Kotlin Multiplatform
Kotlin Multiplataforma permite compartir la capa de datos con otras plataformas. Aprende a configurar y trabajar con DataStore en KMP

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 usas SharedPreferences para almacenar datos, considera migrar a DataStore.

API de DataStore

La interfaz DataStore proporciona la siguiente API:

  1. Es un flujo que se puede usar para leer datos de DataStore.

    val data: Flow<T>
    
  2. Una función para actualizar datos en DataStore

    suspend updateData(transform: suspend (t) -> T)
    

Configuraciones de DataStore

Si deseas almacenar datos y acceder a ellos con claves, usa la implementación de Preferences DataStore, que no requiere un esquema predefinido y no proporciona seguridad de tipos. Tiene una API similar a SharedPreferences, pero no tiene los inconvenientes asociados con las preferencias compartidas.

DataStore te permite conservar clases personalizadas. Para ello, debes definir un esquema para los datos y proporcionar un Serializer para convertirlos en un formato persistente. Puedes elegir usar búferes de protocolo, JSON o cualquier otra estrategia de serialización.

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

Agrega las siguientes líneas a la parte de dependencias de tu archivo de Gradle:

Groovy

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

        // Alternatively - without an Android dependency.
        implementation "androidx.datastore:datastore-preferences-core:1.1.7"
    }
    

Kotlin

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

        // Alternatively - without an Android dependency.
        implementation("androidx.datastore:datastore-preferences-core:1.1.7")
    }
    

Para agregar compatibilidad opcional con RxJava, agrega las siguientes dependencias:

Groovy

    dependencies {
        // optional - RxJava2 support
        implementation "androidx.datastore:datastore-preferences-rxjava2:1.1.7"

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

Kotlin

    dependencies {
        // optional - RxJava2 support
        implementation("androidx.datastore:datastore-preferences-rxjava2:1.1.7")

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

DataStore

Agrega las siguientes líneas a la parte de dependencias de tu archivo de Gradle:

Groovy

    dependencies {
        // Typed DataStore for custom data objects (for example, using Proto or JSON).
        implementation "androidx.datastore:datastore:1.1.7"

        // Alternatively - without an Android dependency.
        implementation "androidx.datastore:datastore-core:1.1.7"
    }
    

Kotlin

    dependencies {
        // Typed DataStore for custom data objects (for example, using Proto or JSON).
        implementation("androidx.datastore:datastore:1.1.7")

        // Alternatively - without an Android dependency.
        implementation("androidx.datastore:datastore-core:1.1.7")
    }
    

Agrega las siguientes dependencias opcionales para la compatibilidad con RxJava:

Groovy

    dependencies {
        // optional - RxJava2 support
        implementation "androidx.datastore:datastore-rxjava2:1.1.7"

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

Kotlin

    dependencies {
        // optional - RxJava2 support
        implementation("androidx.datastore:datastore-rxjava2:1.1.7")

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

Para serializar contenido, agrega dependencias para la serialización de Protocol Buffers o JSON.

Serialización de JSON

Para usar la serialización de JSON, agrega lo siguiente a tu archivo de Gradle:

Groovy

    plugins {
        id("org.jetbrains.kotlin.plugin.serialization") version "2.2.20"
    }

    dependencies {
        implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0"
    }
    

Kotlin

    plugins {
        id("org.jetbrains.kotlin.plugin.serialization") version "2.2.20"
    }

    dependencies {
        implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
    }
    

Serialización de Protobuf

Para usar la serialización de Protobuf, agrega lo siguiente a tu archivo de Gradle:

Groovy

    plugins {
        id("com.google.protobuf") version "0.9.5"
    }
    dependencies {
        implementation "com.google.protobuf:protobuf-kotlin-lite:4.32.1"

    }

    protobuf {
        protoc {
            artifact = "com.google.protobuf:protoc:4.32.1"
        }
        generateProtoTasks {
            all().forEach { task ->
                task.builtins {
                    create("java") {
                        option("lite")
                    }
                    create("kotlin")
                }
            }
        }
    }
    

Kotlin

    plugins {
        id("com.google.protobuf") version "0.9.5"
    }
    dependencies {
        implementation("com.google.protobuf:protobuf-kotlin-lite:4.32.1")
    }

    protobuf {
        protoc {
            artifact = "com.google.protobuf:protoc:4.32.1"
        }
        generateProtoTasks {
            all().forEach { task ->
                task.builtins {
                    create("java") {
                        option("lite")
                    }
                    create("kotlin")
                }
            }
        }
    }
    

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<T> debe ser inmutable. La mutación de un tipo usado en DataStore invalida la coherencia que proporciona DataStore y crea errores potencialmente graves y difíciles de detectar. Te recomendamos que uses búferes de protocolo, que ayudan a garantizar la inmutabilidad, una API clara y una serialización eficiente.

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

Definición de datos

Preferences DataStore

Define una clave que se usará para conservar los datos en el disco.

val EXAMPLE_COUNTER = intPreferencesKey("example_counter")

DataStore JSON

En el caso del almacén de datos JSON, agrega una anotación @Serialization a los datos que deseas conservar.

@Serializable
data class Settings(
    val exampleCounter: Int
)

Define una clase que implemente Serializer<T>, en la que T es el tipo de la clase a la que agregaste la anotación anterior. Asegúrate de incluir un valor predeterminado para que el serializador se use si aún no se creó un archivo.

object SettingsSerializer : Serializer<Settings> {

    override val defaultValue: Settings = Settings(exampleCounter = 0)

    override suspend fun readFrom(input: InputStream): Settings =
        try {
            Json.decodeFromString<Settings>(
                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(t)
                .encodeToByteArray()
        )
    }
}

Proto DataStore

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

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.

Agrega un archivo llamado settings.proto dentro de la carpeta src/main/proto:

syntax = "proto3";

option java_package = "com.example.datastore.snippets.proto";
option java_multiple_files = true;

message Settings {
  int32 example_counter = 1;
}

Define una clase que implemente Serializer<T>, en la que T es el tipo definido en el archivo proto. Esta clase de serializador define cómo DataStore lee y escribe tu tipo de datos. Asegúrate de incluir un valor predeterminado para que el serializador se use si aún no se creó un archivo.

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) {
        return t.writeTo(output)
    }
}

Crea un DataStore

Debes especificar un nombre para el archivo que se usa para conservar los datos.

Preferences DataStore

La implementación de Preferences DataStore usa las clases DataStore y Preferences para conservar pares clave-valor en el disco. 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. Accede a DataStore 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. Como alternativa, usa RxPreferenceDataStoreBuilder si usas RxJava. El parámetro obligatorio name es el nombre de Preferences DataStore.

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

DataStore JSON

Usa el delegado de propiedad que creó dataStore para crear una instancia de DataStore<T>, en la que T es la clase de datos serializable. Llámalo una vez en el nivel superior de tu archivo de Kotlin y accede a él a través de 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.

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

Proto DataStore

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 a través de 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.

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

Lee desde DataStore

Debes especificar un nombre para el archivo que se usa para conservar los datos.

Preferences DataStore

Como 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 con un Flow.

fun counterFlow(): Flow<Int> = context.dataStore.data.map { preferences ->
    preferences[EXAMPLE_COUNTER] ?: 0
}

DataStore JSON

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

fun counterFlow(): Flow<Int> = context.dataStore.data.map { settings ->
    settings.exampleCounter
}

Proto DataStore

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

fun counterFlow(): Flow<Int> = context.dataStore.data.map { settings ->
    settings.exampleCounter
}

Escribe en DataStore

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. Todo el código del bloque updateData se trata como una sola transacción.

Preferences DataStore

suspend fun incrementCounter() {
    context.dataStore.updateData {
        it.toMutablePreferences().also { preferences ->
            preferences[EXAMPLE_COUNTER] = (preferences[EXAMPLE_COUNTER] ?: 0) + 1
        }
    }
}

DataStore JSON

suspend fun incrementCounter() {
    context.dataStore.updateData { settings ->
        settings.copy(exampleCounter = settings.exampleCounter + 1)
    }
}

Proto DataStore

suspend fun incrementCounter() {
    context.dataStore.updateData { settings ->
        settings.copy { exampleCounter = exampleCounter + 1 }
    }
}

Muestra de Compose

Puedes juntar estas funciones en una clase y usarla en una app de Compose.

Preferences DataStore

Ahora podemos colocar estas funciones en una clase llamada PreferencesDataStore y usarla en una app de Compose.

val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val preferencesDataStore = remember(context) { PreferencesDataStore(context) }

// Display counter value.
val exampleCounter by preferencesDataStore.counterFlow()
    .collectAsState(initial = 0, coroutineScope.coroutineContext)
Text(
    text = "Counter $exampleCounter",
    fontSize = 25.sp
)

// Update the counter.
Button(
    onClick = {
        coroutineScope.launch { preferencesDataStore.incrementCounter() }
    }
) {
    Text("increment")
}

DataStore JSON

Ahora podemos colocar estas funciones en una clase llamada JSONDataStore y usarla en una app de Compose.

val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val jsonDataStore = remember(context) { JsonDataStore(context) }

// Display counter value.
val exampleCounter by jsonDataStore.counterFlow()
    .collectAsState(initial = 0, coroutineScope.coroutineContext)
Text(
    text = "Counter $exampleCounter",
    fontSize = 25.sp
)

// Update the counter.
Button(onClick = { coroutineScope.launch { jsonDataStore.incrementCounter() } }) {
    Text("increment")
}

Proto DataStore

Ahora podemos colocar estas funciones en una clase llamada ProtoDataStore y usarla en una app de Compose.

val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val protoDataStore = remember(context) { ProtoDataStore(context) }

// Display counter value.
val exampleCounter by protoDataStore.counterFlow()
    .collectAsState(initial = 0, coroutineScope.coroutineContext)
Text(
    text = "Counter $exampleCounter",
    fontSize = 25.sp
)

// Update the counter.
Button(onClick = { coroutineScope.launch { protoDataStore.incrementCounter() } }) {
    Text("increment")
}

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 una IU sin respuesta. 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 propiedades de coherencia de datos que brinda un solo proceso. En particular, DataStore proporciona 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.

Considera una aplicación de ejemplo con un servicio y una actividad en la que el servicio se ejecuta en un proceso separado y actualiza el almacén de datos de forma periódica.

En este ejemplo, se usa un almacén de datos JSON, pero también puedes usar un almacén de datos de preferencias o de proto.

@Serializable
data class Time(
    val lastUpdateMillis: Long
)

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

object TimeSerializer : Serializer<Time> {

    override val defaultValue: Time = Time(lastUpdateMillis = 0L)

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

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

Para usar DataStore en diferentes procesos, debes construir el objeto DataStore con MultiProcessDataStoreFactory para el código de la app y el servicio:

val dataStore = MultiProcessDataStoreFactory.create(
    serializer = TimeSerializer,
    produceFile = {
        File("${context.cacheDir.path}/time.pb")
    },
    corruptionHandler = null
)

Agrega lo siguiente a tu AndroidManifiest.xml:

<service
    android:name=".TimestampUpdateService"
    android:process=":my_process_id" />

El servicio llama periódicamente a updateLastUpdateTime(), que escribe en el almacén de datos con updateData.

suspend fun updateLastUpdateTime() {
    dataStore.updateData { time ->
        time.copy(lastUpdateMillis = System.currentTimeMillis())
    }
}

La app lee el valor que escribió el servicio con el flujo de datos:

fun timeFlow(): Flow<Long> = dataStore.data.map { time ->
    time.lastUpdateMillis
}

Ahora, podemos juntar todas estas funciones en una clase llamada MultiProcessDataStore y usarla en una app.

Este es el código de servicio:

class TimestampUpdateService : Service() {
    val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
    val multiProcessDataStore by lazy { MultiProcessDataStore(applicationContext) }


    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        serviceScope.launch {
            while (true) {
                multiProcessDataStore.updateLastUpdateTime()
                delay(1000)
            }
        }
        return START_NOT_STICKY
    }

    override fun onDestroy() {
        super.onDestroy()
        serviceScope.cancel()
    }
}

Y el código de la app:

val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val multiProcessDataStore = remember(context) { MultiProcessDataStore(context) }

// Display time written by other process.
val lastUpdateTime by multiProcessDataStore.timeFlow()
    .collectAsState(initial = 0, coroutineScope.coroutineContext)
Text(
    text = "Last updated: $lastUpdateTime",
    fontSize = 25.sp
)

DisposableEffect(context) {
    val serviceIntent = Intent(context, TimestampUpdateService::class.java)
    context.startService(serviceIntent)
    onDispose {
        context.stopService(serviceIntent)
    }
}

Puedes usar la inserción de dependencias de Hilt para que tu instancia de DataStore sea única por proceso:

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

Cómo controlar la corrupción de archivos

En ocasiones excepcionales, es posible que se dañe el archivo persistente en el disco de DataStore. De forma predeterminada, DataStore no se recupera automáticamente de la corrupción, y los intentos de lectura desde él harán que el sistema arroje un CorruptionException.

DataStore ofrece una API de controlador de corrupción que puede ayudarte a recuperarte correctamente en ese caso y evitar que se arroje la excepción. Cuando se configura, el controlador de corrupción 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

Comparte tus comentarios e ideas con nosotros por medio de estos recursos:

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