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 Flow 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 DataStore interface 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 tipos. Ele tem uma API semelhante a SharedPreferences, mas não
tem as desvantagens associadas a preferências compartilhadas.
O DataStore permite manter classes personalizadas. Para fazer 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.2.1" // Alternatively - without an Android dependency. implementation "androidx.datastore:datastore-preferences-core:1.2.1" }
Kotlin
dependencies { // Preferences DataStore (SharedPreferences like APIs) implementation("androidx.datastore:datastore-preferences:1.2.1") // Alternatively - without an Android dependency. implementation("androidx.datastore:datastore-preferences-core:1.2.1") }
Para adicionar suporte opcional ao RxJava, adicione as seguintes dependências:
Groovy
dependencies { // optional - RxJava2 support implementation "androidx.datastore:datastore-preferences-rxjava2:1.2.1" // optional - RxJava3 support implementation "androidx.datastore:datastore-preferences-rxjava3:1.2.1" }
Kotlin
dependencies { // optional - RxJava2 support implementation("androidx.datastore:datastore-preferences-rxjava2:1.2.1") // optional - RxJava3 support implementation("androidx.datastore:datastore-preferences-rxjava3:1.2.1") }
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.2.1" // Alternatively - without an Android dependency. implementation "androidx.datastore:datastore-core:1.2.1" }
Kotlin
dependencies { // Typed DataStore for custom data objects (for example, using Proto or JSON). implementation("androidx.datastore:datastore:1.2.1") // Alternatively - without an Android dependency. implementation("androidx.datastore:datastore-core:1.2.1") }
Adicione as seguintes dependências opcionais para compatibilidade com RxJava:
Groovy
dependencies { // optional - RxJava2 support implementation "androidx.datastore:datastore-rxjava2:1.2.1" // optional - RxJava3 support implementation "androidx.datastore:datastore-rxjava3:1.2.1" }
Kotlin
dependencies { // optional - RxJava2 support implementation("androidx.datastore:datastore-rxjava2:1.2.1") // optional - RxJava3 support implementation("androidx.datastore:datastore-rxjava3:1.2.1") }
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 da forma correta
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 o uso de buffers de protocolo, que ajudam a garantir a imutabilidade, uma API clara e uma serialização eficiente.Não misture usos de
SingleProcessDataStoreeMultiProcessDataStorepara o 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 repositório 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 serializador a 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
É necessário especificar um nome para o arquivo usado para manter os dados.
Preferences DataStore
A implementação Preferences DataStore usa as classes DataStore e
Preferences para manter pares de chave-valor no disco. 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. Acesse o DataStore usando essa propriedade em todo o restante do aplicativo. Isso facilita a manutenção do DataStore como um Singleton.
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 anteriormente.
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 anteriormente.
val Context.dataStore: DataStore<Settings> by dataStore(
fileName = "settings.pb",
serializer = SettingsSerializer,
)
Ler do DataStore
É necessário 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 você precisa armazenar na instância DataStore<Preferences>. Por exemplo, para definir
uma chave para um valor inteiro, use intPreferencesKey. Em seguida, use a
DataStore.data propriedade para expor o valor armazenado apropriado usando um
fluxo.
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
}
Use collectAsStateWithLifecycle para consumir o Flow produzido por
um ViewModel em um elemento combinável.
Isso converte com segurança o fluxo do DataStore em um estado do Compose que aciona a recomposição.
@Composable
fun SomeScreen(counterFlow: Flow<Int>) {
val counter by counterFlow.collectAsStateWithLifecycle(initialValue = 0)
Text(text = "Example counter: ${counter}")
}
Para mais informações sobre collectAsStateWithLifecycle,
consulte Estado e Jetpack Compose.
Gravar no DataStore
O 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 atualiza os dados 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 }
}
}
Usar o DataStore em um app do Compose
Para usar o DataStore em um app do Compose, siga as diretrizes de arquitetura de apps Android, mantendo as operações do DataStore na camada de dados (como um repositório) e expondo os dados à interface usando um ViewModel.
Evite ler ou gravar no DataStore diretamente nas funções combináveis.
Exponha o DataStore usando um ViewModel. Transmita seu repositório (que envolve o DataStore) para o
ViewModele converta oFlowem umStateFlowpara que a interface possa observá-lo facilmente, conforme mostrado no snippet a seguir:class SettingsViewModel( private val userPreferencesRepository: UserPreferencesRepository ) : ViewModel() { // Expose the DataStore flow as a StateFlow for Compose val userSettings: StateFlow<UserSettings> = userPreferencesRepository.userSettingsFlow .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5000), initialValue = UserSettings.getDefaultInstance() ) fun updateCounter(newValue: Int) { viewModelScope.launch { userPreferencesRepository.updateCounter(newValue) } } }Observe e grave no elemento combinável. Use
collectAsStateWithLifecyclepara observar com segurança oStateFlowna interface e chame as funçõesViewModelpara processar gravações, conforme mostrado no snippet a seguir:@Composable fun SettingsScreen( viewModel: SettingsViewModel = viewModel() ) { // Safely collect the state val settings by viewModel.userSettings.collectAsStateWithLifecycle() Column(modifier = Modifier.padding(16.dp)) { Text(text = "Current counter: ${settings.counter}") Spacer(modifier = Modifier.height(8.dp)) Button(onClick = { viewModel.updateCounter(settings.counter + 1) }) { Text("Increment Counter") } } }
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 fornece as seguintes propriedades:
- As leituras retornam apenas os dados mantidos no disco.
- Consistência de leitura após gravação.
- 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 também é possível usar um Preferences ou Proto DataStore.
@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 a ser usado se ainda não houver arquivos criados. Consulte 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, você precisa criar
o objeto DataStore usando a MultiProcessDataStoreFactory para o app
e o código de serviço:
val dataStore = MultiProcessDataStoreFactory.create(
serializer = TimeSerializer,
produceFile = {
File("${context.filesDir.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 updateLastUpdateTime periodicamente, 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 reunir todas essas funções em uma classe chamada MultiProcessDataStore e usá-la em um app.
Confira o código de 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 de 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 do DataStore no disco pode ser corrompido. Por padrão, o DataStore não se recupera automaticamente da corrupção, e as tentativas de leitura causam a geração de uma CorruptionException pelo sistema.
O DataStore oferece uma API de gerenciador de corrupção que pode ajudar você a se recuperar normalmente em um cenário como esse e evitar a geração da 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 gerenciador, 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.filesDir.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