DataStore
Parte do Android Jetpack.
Mantenha tudo organizado com as coleções
Salve e categorize o conteúdo com base nas suas preferências.
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.
Como usar o DataStore da forma correta
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
precisa ser imutável . A mutação de um tipo usado no DataStore invalida todas as garantias fornecidas e cria bugs potencialmente graves e difíceis de detectar. É altamente recomendável usar buffers de protocolo que ofereçam garantias de imutabilidade, uma API simples e uma serialização eficiente.Nunca misture os usos do
SingleProcessDataStore
e doMultiProcessDataStore
no mesmo arquivo. Se você pretende acessar oDataStore
em mais de um processo, sempre useMultiProcessDataStore
.
Configurar
Para usar o Jetpack DataStore no seu app, adicione o seguinte ao arquivo Gradle, dependendo da implementação que você quer usar:
Preferences DataStore
// Preferences DataStore (SharedPreferences like APIs) dependencies { implementation "androidx.datastore:datastore-preferences:1.1.5" // optional - RxJava2 support implementation "androidx.datastore:datastore-preferences-rxjava2:1.1.5" // optional - RxJava3 support implementation "androidx.datastore:datastore-preferences-rxjava3:1.1.5" } // Alternatively - use the following artifact without an Android dependency. dependencies { implementation "androidx.datastore:datastore-preferences-core:1.1.5" }
// Preferences DataStore (SharedPreferences like APIs) dependencies { implementation("androidx.datastore:datastore-preferences:1.1.5") // optional - RxJava2 support implementation("androidx.datastore:datastore-preferences-rxjava2:1.1.5") // optional - RxJava3 support implementation("androidx.datastore:datastore-preferences-rxjava3:1.1.5") } // Alternatively - use the following artifact without an Android dependency. dependencies { implementation("androidx.datastore:datastore-preferences-core:1.1.5") }
Proto DataStore
// Typed DataStore (Typed API surface, such as Proto) dependencies { implementation "androidx.datastore:datastore:1.1.5" // optional - RxJava2 support implementation "androidx.datastore:datastore-rxjava2:1.1.5" // optional - RxJava3 support implementation "androidx.datastore:datastore-rxjava3:1.1.5" } // Alternatively - use the following artifact without an Android dependency. dependencies { implementation "androidx.datastore:datastore-core:1.1.5" }
// Typed DataStore (Typed API surface, such as Proto) dependencies { implementation("androidx.datastore:datastore:1.1.5") // optional - RxJava2 support implementation("androidx.datastore:datastore-rxjava2:1.1.5") // optional - RxJava3 support implementation("androidx.datastore:datastore-rxjava3:1.1.5") } // Alternatively - use the following artifact without an Android dependency. dependencies { implementation("androidx.datastore:datastore-core:1.1.5") }
Armazenar pares de chave-valor com Preferences DataStore
A implementação Preferences DataStore usa as classes
DataStore
e
Preferences
para manter pares simples 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.
// At the top level of your kotlin file: val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
RxDataStore<Preferences> dataStore = new RxPreferenceDataStoreBuilder(context, /*name=*/ "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
.
val EXAMPLE_COUNTER = intPreferencesKey("example_counter") val exampleCounterFlow: Flow<Int> = context.dataStore.data .map { preferences -> // No type safety. preferences[EXAMPLE_COUNTER] ?: 0 }
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.
suspend fun incrementCounter() { context.dataStore.edit { settings -> val currentCounterValue = settings[EXAMPLE_COUNTER] ?: 0 settings[EXAMPLE_COUNTER] = currentCounterValue + 1 } }
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
informa ao DataStore o nome da classe do serializador definida na etapa 1.
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 )
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();
Ler em um Proto DataStore
Use DataStore.data
para expor um Flow
da propriedade apropriada do seu objeto armazenado.
val exampleCounterFlow: Flow<Int> = context.settingsDataStore.data .map { settings -> // The exampleCounter property is generated from the proto schema. settings.exampleCounter }
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.
suspend fun incrementCounter() { context.settingsDataStore.updateData { currentSettings -> currentSettings.toBuilder() .setExampleCounter(currentSettings.exampleCounter + 1) .build() } }
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()
(link em inglês)
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:
val exampleData = runBlocking { context.dataStore.data.first() }
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 instabilidade da UI. É possível atenuar esses problemas carregando antecipadamente os dados do DataStore:
override fun onCreate(savedInstanceState: Bundle?) { lifecycleScope.launch { context.dataStore.data.first() // You should also handle IOExceptions here. } }
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 garantias de consistência presentes em um único processo. Especificamente, o DataStore garante que:
- 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 que
será usado se ainda não houver arquivos criados. Consulte 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 de Hilt para garantir que a instância do DataStore seja única para cada processo:
@Provides
@Singleton
fun provideDataStore(@ApplicationContext context: Context): DataStore<Settings> =
MultiProcessDataStoreFactory.create(...)
Processar a corrupção de arquivos
Em raras ocasiões, o arquivo persistente do DataStore no disco pode ficar
corrompido. Por padrão, o DataStore não se recupera automaticamente de corrupção,
e as tentativas de leitura dele fazem com que o sistema gere uma
CorruptionException
.
O DataStore oferece uma API de gerenciador de corrupção que pode ajudar você a se recuperar nesse cenário e evitar a exceção. Quando configurado, o gerenciador de corrupção substitui o arquivo corrompido por um novo que contém um valor padrão predefinido.
Para configurar esse manipulador, forneça um corruptionHandler
ao criar a
instância do 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
Learn how this app was designed and built in the design case study, architecture learning journey and modularization learning journey.
This is the repository for the Now in Android app. It is a work in progress 🚧.
Now in Android is a fully functionalNow in Android App
Blogs
- Dar preferência ao armazenamento de dados com o Jetpack DataStore (link em inglês)
Codelabs
Recomendados para você
Carregar e exibir dados paginados
Discover the latest app development tools, platform updates, training, and documentation for developers across every Android device.
Visão geral do LiveData
Use o LiveData para lidar com dados considerando o ciclo de vida.
Layouts e expressões de vinculação
Discover the latest app development tools, platform updates, training, and documentation for developers across every Android device.