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.
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:
Um fluxo que pode ser usado para ler dados do DataStore.
val data: Flow<T>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:
Nunca crie mais de uma instância do
DataStorepara 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 gerarIllegalStateExceptionao ler ou atualizar dados.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.Não misture os usos de
SingleProcessDataStoreeMultiProcessDataStoreno mesmo arquivo. Se você pretende acessar oDataStoreem mais de um processo, useMultiProcessDataStore.
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
- Dar preferência ao armazenamento de dados com o Jetpack DataStore (link em inglês)
Codelabs
Recomendados para você
- Observação: o texto do link aparece quando o JavaScript está desativado.
- Carregar e exibir dados paginados
- Visão geral do LiveData
- Layouts e expressões de vinculação