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.
Preferences DataStore e Proto DataStore
O DataStore oferece duas implementações diferentes: Preferences DataStore e Proto DataStore.
- O Preferences DataStore armazena e acessa dados usando chaves. Essa implementação não requer um esquema predefinido e não fornece segurança de tipo.
- O Proto DataStore armazena dados como instâncias de um tipo de dados personalizado. Essa implementação requer a definição de um esquema usando buffers de protocolo, mas fornece segurança de tipos.
Usar o DataStore corretamente
Para usar o DataStore da forma correta, siga estas regras:
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 gerarIllegalStateException
ao 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
SingleProcessDataStore
eMultiProcessDataStore
no mesmo arquivo. Se você pretende acessar oDataStore
em mais de um processo, useMultiProcessDataStore
.
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
Groovy
// Preferences DataStore (SharedPreferences like APIs) dependencies { implementation "androidx.datastore:datastore-preferences:1.1.7" // optional - RxJava2 support implementation "androidx.datastore:datastore-preferences-rxjava2:1.1.7" // optional - RxJava3 support implementation "androidx.datastore:datastore-preferences-rxjava3:1.1.7" } // Alternatively - use the following artifact without an Android dependency. dependencies { implementation "androidx.datastore:datastore-preferences-core:1.1.7" }
Kotlin
// Preferences DataStore (SharedPreferences like APIs) dependencies { implementation("androidx.datastore:datastore-preferences:1.1.7") // optional - RxJava2 support implementation("androidx.datastore:datastore-preferences-rxjava2:1.1.7") // optional - RxJava3 support implementation("androidx.datastore:datastore-preferences-rxjava3:1.1.7") } // Alternatively - use the following artifact without an Android dependency. dependencies { implementation("androidx.datastore:datastore-preferences-core:1.1.7") }
Proto DataStore
Groovy
// Typed DataStore (Typed API surface, such as Proto) dependencies { implementation "androidx.datastore:datastore:1.1.7" // optional - RxJava2 support implementation "androidx.datastore:datastore-rxjava2:1.1.7" // optional - RxJava3 support implementation "androidx.datastore:datastore-rxjava3:1.1.7" } // Alternatively - use the following artifact without an Android dependency. dependencies { implementation "androidx.datastore:datastore-core:1.1.7" }
Kotlin
// Typed DataStore (Typed API surface, such as Proto) dependencies { implementation("androidx.datastore:datastore:1.1.7") // optional - RxJava2 support implementation("androidx.datastore:datastore-rxjava2:1.1.7") // optional - RxJava3 support implementation("androidx.datastore:datastore-rxjava3:1.1.7") } // Alternatively - use the following artifact without an Android dependency. dependencies { implementation("androidx.datastore:datastore-core:1.1.7") }
Armazenar pares de valor-chave com Preferences DataStore
A implementação do Preferences DataStore usa as classes DataStore
e
Preferences
para manter pares de chave-valor no disco.
Criar um Preferences DataStore
Use a delegação da 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 e acesse-a usando essa propriedade em todo o restante do
aplicativo. Isso facilita a manutenção de DataStore
como um Singleton.
Como alternativa, use RxPreferenceDataStoreBuilder
se estiver usando RxJava.
O parâmetro name
obrigatório é o nome do 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, "settings").build();
Ler em um 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
.
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));
Gravar em um Preferences DataStore
O Preferences DataStore disponibiliza uma função edit()
que atualiza os dados de forma transacional
em um DataStore
. O parâmetro transform
da função aceita
um bloco de código em que você pode atualizar os valores conforme necessário. Todo o código no bloco de transformação é tratado como uma única transação.
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.
Armazenar objetos tipados com Proto DataStore
A implementação Proto DataStore usa DataStore e buffers de protocolo para manter objetos tipados no disco.
Definir um esquema
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).
syntax = "proto3";
option java_package = "com.example.application.proto";
option java_multiple_files = true;
message Settings {
int32 example_counter = 1;
}
Criar um Proto DataStore
Há duas etapas envolvidas na criação de um Proto DataStore para armazenar os objetos tipados:
- Defina uma classe que implemente
Serializer<T>
, em queT
é o tipo definido no arquivo proto. Essa classe de serializador informa ao DataStore como ler e gravar o tipo de dados. Inclua um valor padrão para o serializador a ser usado se ainda não houver arquivos criados. - Use a delegação de propriedade criada por
dataStore
para criar uma instância deDataStore<T>
em queT
é 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âmetrofilename
informa ao DataStore qual arquivo usar para armazenar os dados, e o parâmetroserializer
especifica o nome da classe do serializador definida na etapa 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() {
return Settings.getDefaultInstance();
}
@Override
public Settings readFrom(@NotNull InputStream input) {
try {
return Settings.parseFrom(input);
} catch (InvalidProtocolBufferException exception) {
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();
Ler em um Proto DataStore
Use DataStore.data
para expor um Flow
da propriedade apropriada do seu
objeto armazenado.
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());
Gravar em um Proto DataStore
O Proto DataStore disponibiliza uma função
updateData()
que atualiza transacionalmente 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.
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()));
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:
O serviço está sendo executado em um processo separado e atualiza periodicamente o DataStore.
<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) } } }
Já o app coleta essas mudanças e atualiza a interface.
val settings: Settings by dataStore.data.collectAsState() Text( text = "Last updated: $${settings.timestamp}", )
Para usar o DataStore em diferentes processos, você precisa criar
o objeto dele usando a MultiProcessDataStoreFactory
.
val dataStore: DataStore<Settings> = MultiProcessDataStoreFactory.create(
serializer = SettingsSerializer(),
produceFile = {
File("${context.cacheDir.path}/myapp.preferences_pb")
}
)
O serializer
informa ao DataStore como ler e gravar o tipo de dados. Inclua um valor padrão para o serializador a ser usado se ainda não houver arquivos criados. Confira abaixo um exemplo de implementação usando a
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): Settings =
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()
)
}
}
É 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