Arquitetura do app: camada de dados – DataStore – Desenvolvedores Android

Projeto: /architecture/_project.yaml Livro: /architecture/_book.yaml Palavras-chave: datastore, arquitetura, api:JetpackDataStore description: Consulte este guia de arquitetura do app sobre bibliotecas de camada de dados para saber mais sobre o Preferences DataStore, Proto DataStore, configuração e muito mais. hide_page_heading: true

DataStore   Parte do Android Jetpack.

Teste com o Kotlin Multiplatform
O Kotlin Multiplatform permite compartilhar a camada de dados com outras plataformas. Saiba como configurar e trabalhar com o DataStore no KMP

O Jetpack DataStore é uma solução de armazenamento de dados que permite armazenar pares de chave-valor ou objetos tipados com buffers de protocolo. O DataStore usa corrotinas e fluxo do Kotlin para armazenar dados de forma assíncrona, consistente e transacional.

Se você estiver usando SharedPreferences para armazenar dados, considere migrar para o DataStore.

API DataStore

A interface DataStore fornece a seguinte API:

  1. Um fluxo que pode ser usado para ler dados do DataStore.

    val data: Flow<T>
    
  2. Uma função para atualizar dados no DataStore

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

Configurações do DataStore

Se você quiser armazenar e acessar dados usando chaves, use a implementação do Preferences DataStore, que não exige um esquema predefinido e não oferece segurança de tipo. Ela tem uma API semelhante a SharedPreferences, mas não tem as desvantagens associadas às preferências compartilhadas.

O DataStore permite manter classes personalizadas. Para isso, defina um esquema para os dados e forneça um Serializer para convertê-los em um formato persistente. Você pode usar buffers de protocolo, JSON ou qualquer outra estratégia de serialização.

Configuração

Para usar o Jetpack DataStore no seu app, adicione o seguinte ao arquivo Gradle, dependendo da implementação que você quer usar:

Preferences DataStore

Adicione as seguintes linhas à parte de dependências do arquivo 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 adicionar suporte opcional ao RxJava, adicione as seguintes dependências:

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

Adicione as seguintes linhas à parte de dependências do arquivo 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")
    }
    

Adicione as seguintes dependências opcionais para compatibilidade com 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 conteúdo, adicione dependências para buffers de protocolo ou serialização JSON.

Serialização JSON

Para usar a serialização JSON, adicione o seguinte ao arquivo do 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")
    }
    

Serialização do Protobuf

Para usar a serialização do Protobuf, adicione o seguinte ao arquivo do 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")
                }
            }
        }
    }
    

Usar o DataStore corretamente

Para usar o DataStore da forma correta, siga estas regras:

  1. Nunca crie mais de uma instância do DataStore para um determinado arquivo no mesmo processo. Essa ação pode interromper toda a funcionalidade do DataStore. Se houver vários DataStores ativos para um determinado arquivo no mesmo processo, o DataStore vai gerar IllegalStateException ao ler ou atualizar dados.

  2. O tipo genérico do DataStore<T> precisa ser imutável. A mutação de um tipo usado no DataStore invalida a consistência fornecida e cria bugs potencialmente graves e difíceis de detectar. Recomendamos que você use buffers de protocolo, que ajudam a garantir imutabilidade, uma API clara e serialização eficiente.

  3. Não misture os usos de SingleProcessDataStore e MultiProcessDataStore no mesmo arquivo. Se você pretende acessar o DataStore em mais de um processo, use MultiProcessDataStore.

Definição de dados

Preferences DataStore

Defina uma chave que será usada para manter os dados no disco.

val EXAMPLE_COUNTER = intPreferencesKey("example_counter")

JSON DataStore

Para o armazenamento de dados JSON, adicione uma anotação @Serialization aos dados que você quer manter.

@Serializable
data class Settings(
    val exampleCounter: Int
)

Defina uma classe que implemente Serializer<T>, em que T é o tipo da classe a que você adicionou a anotação anterior. Inclua um valor padrão para o serializador a ser usado se ainda não houver arquivos criados.

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

A implementação Proto DataStore usa DataStore e buffers de protocolo para manter objetos tipados no disco.

O Proto DataStore requer um esquema predefinido em um arquivo proto no diretório app/src/main/proto/. Esse esquema define o tipo dos objetos que você persiste no Proto DataStore. Para saber mais sobre como definir um esquema proto, consulte o guia de linguagem do protobuf (em inglês).

Adicione um arquivo chamado settings.proto na pasta src/main/proto:

syntax = "proto3";

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

message Settings {
  int32 example_counter = 1;
}

Defina uma classe que implemente Serializer<T>, em que T é o tipo definido no arquivo proto. Essa classe de serializador define como o DataStore lê e grava o tipo de dados. Inclua um valor padrão para o serializer que será usado se ainda não houver arquivos criados.

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)
    }
}

Criar um DataStore

É preciso especificar um nome para o arquivo usado para manter os dados.

Preferences DataStore

A implementação do Preferences DataStore usa as classes DataStore e Preferences para manter pares de chave-valor no disco. Use a delegação de propriedade criada por preferencesDataStore para criar uma instância de DataStore<Preferences>. Faça a chamada uma vez no nível superior do arquivo Kotlin. Acesse o DataStore usando essa propriedade em todo o restante do aplicativo. Isso facilita a manutenção do DataStore como um Singleton. Como alternativa, use RxPreferenceDataStoreBuilder se estiver usando RxJava. O parâmetro name obrigatório é o nome do Preferences DataStore.

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

JSON DataStore

Use a delegação de propriedade criada por dataStore para criar uma instância de DataStore<T>, em que T é a classe de dados serializável. Faça a chamada uma vez no nível superior do arquivo Kotlin e acesse-a usando a delegação de propriedade em todo o restante do app. O parâmetro fileName informa ao DataStore qual arquivo usar para armazenar os dados, e o parâmetro serializer informa ao DataStore o nome da classe do serializador definida na etapa 1.

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

Proto DataStore

Use a delegação de propriedade criada por dataStore para criar uma instância de DataStore<T>, em que T é o tipo definido no arquivo proto. Faça a chamada uma vez no nível superior do arquivo Kotlin e acesse-a usando a delegação de propriedade em todo o restante do app. O parâmetro fileName informa ao DataStore qual arquivo usar para armazenar os dados, e o parâmetro serializer informa ao DataStore o nome da classe do serializador definida na etapa 1.

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

Ler do DataStore

É preciso especificar um nome para o arquivo usado para manter os dados.

Preferences DataStore

Como o Preferences DataStore não usa um esquema predefinido, é necessário usar a função de tipo de chave correspondente para definir uma chave para cada valor que precisa armazenar na instância DataStore<Preferences>. Por exemplo, para definir uma chave para um valor de int, use intPreferencesKey(). Em seguida, use a propriedade DataStore.data para expor o valor armazenado apropriado usando um Flow.

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

JSON DataStore

Use DataStore.data para expor um Flow da propriedade apropriada do seu objeto armazenado.

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

Proto DataStore

Use DataStore.data para expor um Flow da propriedade apropriada do seu objeto armazenado.

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

Gravar no DataStore

O DataStore disponibiliza uma função updateData() que atualiza de forma transacional um objeto armazenado. updateData fornece o estado atual dos dados como uma instância do seu tipo de dados e os atualiza de maneira transacional em uma operação atômica de leitura-gravação-modificação. Todo o código no bloco updateData é tratado como uma única transação.

Preferences DataStore

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

JSON DataStore

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 }
    }
}

Exemplo do Compose

Você pode juntar essas funções em uma classe e usá-la em um app do Compose.

Preferences DataStore

Agora podemos colocar essas funções em uma classe chamada PreferencesDataStore e usá-la em um app do 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")
}

JSON DataStore

Agora podemos colocar essas funções em uma classe chamada JSONDataStore e usá-la em um app do 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

Agora podemos colocar essas funções em uma classe chamada ProtoDataStore e usá-la em um app do 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")
}

Usar o DataStore no código síncrono

Um dos principais benefícios do DataStore é a API assíncrona, mas nem sempre é possível mudar o código circundante para que ele seja assíncrono. Isso poderá acontecer se você estiver trabalhando com uma base de código existente que usa E/S de disco síncrono ou se você tiver uma dependência que não fornece uma API assíncrona.

As corrotinas do Kotlin fornecem o builder de corrotinas runBlocking() para ajudar a preencher a lacuna entre o código síncrono e assíncrono. Você pode usar runBlocking() para ler dados do DataStore de forma síncrona. O RxJava oferece métodos de bloqueio em Flowable. O código a seguir bloqueia a linha de execução de chamada até que o DataStore retorne dados:

Kotlin

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

Java

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

A execução de operações síncronas de E/S na linha de execução de interface pode causar ANRs ou uma interface não responsiva. É possível atenuar esses problemas carregando antecipadamente os dados do DataStore:

Kotlin

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

Java

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

Dessa forma, o DataStore lê de maneira assíncrona os dados e os armazena em cache na memória. As leituras síncronas posteriores que usam runBlocking() podem ser mais rápidas ou podem evitar completamente uma operação de E/S de disco caso a leitura inicial seja concluída.

Usar o DataStore em códigos com vários processos

É possível configurar o DataStore para acessar os mesmos dados em processos diferentes e com as mesmas propriedades de consistência presentes em um único processo. Especificamente, o DataStore oferece:

  • As leituras retornem apenas os dados mantidos no disco.
  • As leituras após gravações sejam consistentes.
  • As gravações sejam serializadas.
  • As leituras nunca sejam bloqueadas por gravações.

Considere um aplicativo de exemplo com um serviço e uma atividade em que o serviço está sendo executado em um processo separado e atualiza periodicamente o DataStore.

Este exemplo usa um repositório de dados JSON, mas você também pode usar um repositório de dados de preferências ou proto.

@Serializable
data class Time(
    val lastUpdateMillis: Long
)

Um serializador informa ao DataStore como ler e gravar o tipo de dados. Inclua um valor padrão para o serializador que será usado se ainda não houver arquivos criados. Confira abaixo um exemplo de implementação usando a 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 em diferentes processos, crie o objeto DataStore usando o MultiProcessDataStoreFactory para o app e o código do serviço:

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

Adicione o seguinte ao seu AndroidManifiest.xml:

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

O serviço chama periodicamente updateLastUpdateTime(), que grava no repositório de dados usando updateData.

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

O app lê o valor gravado pelo serviço usando o fluxo de dados:

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

Agora, podemos juntar todas essas funções em uma classe chamada MultiProcessDataStore e usá-la em um app.

Este é o código do serviço:

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()
    }
}

E o código do 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)
    }
}

É possível usar a injeção de dependências do Hilt para que a instância do DataStore seja única para cada processo:

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

Processar corrupção de arquivos

Em raras ocasiões, o arquivo persistente no disco do DataStore pode ser corrompido. Por padrão, o DataStore não se recupera automaticamente de corrupções, e as tentativas de leitura dele farão com que o sistema gere um CorruptionException.

O DataStore oferece uma API de tratamento de corrupção que pode ajudar você a se recuperar normalmente em um cenário assim e evitar o lançamento da exceção. Quando configurado, o processador de corrupção substitui o arquivo corrompido por um novo com um valor padrão predefinido.

Para configurar esse manipulador, forneça um corruptionHandler ao criar a instância DataStore em by dataStore() ou no 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) }
)

Enviar feedback

Envie comentários e ideias usando os recursos abaixo:

Issue Tracker:
Informe os problemas para que possamos corrigir os bugs.

Outros recursos

Para saber mais sobre o Jetpack DataStore, consulte os seguintes recursos extras:

Exemplos

Blogs

Codelabs