1. Introdução
O que é o Datastore?
O Datastore é uma solução nova e melhorada de armazenamento de dados que visa substituir o SharedPreferences. Criado em corrotinas e fluxo Kotlin, ele oferece duas implementações diferentes: Proto DataStore, que armazena objetos categorizados (com suporte de buffers de protocolo) e Preferences DataStore, que armazena pares de chave-valor. Os dados são armazenados de forma assíncrona, consistente e transacional, superando algumas das desvantagens de SharedPreferences.
O que você aprenderá
- O que é o DataStore e por que usá-lo.
- Como adicionar o DataStore ao seu projeto.
- As diferenças entre o Preferences e o Proto DataStore e as vantagens de cada um.
- Como usar o Preferences DataStore.
- Como migrar de SharedPreferences para o Preferences DataStore.
O que você criará
Neste codelab, você começará com um app de amostra que exibe uma lista de tarefas que podem ser filtradas pelo status concluído e podem ser classificadas por prioridade e prazo.
A sinalização booleana para o filtro Mostrar tarefas concluídas é salva na memória. A ordem de classificação é mantida no disco usando um objeto SharedPreferences
.
Neste codelab, você aprenderá a usar o Preferences DataStore concluindo as seguintes tarefas:
- Manter o filtro de status concluído no DataStore.
- Migrar a ordem de classificação de SharedPreferences para o armazenamento de dados.
Também recomendamos que você use o codelab Proto DataStore para entender melhor a diferença entre os dois.
O que é necessário
- Android Studio Arctic Fox.
- Familiaridade com estes componentes da arquitetura: LiveData, ViewModel, Vinculação de visualizações e com a arquitetura sugerida no Guia para a arquitetura do app.
- Familiaridade com corrotinas e fluxo do Kotlin (links em inglês).
Para uma introdução aos componentes da arquitetura, consulte o codelab Room com View. Para uma introdução ao fluxo, confira o codelab Corrotinas avançadas com fluxo do Kotlin e LiveData.
2. Etapas da configuração
Nesta etapa, você vai fazer o download do código para o codelab inteiro e executar um app simples de exemplo.
Para começar o mais rápido possível, preparamos um projeto inicial para você desenvolver.
Se você tiver o git instalado, basta executar o comando abaixo. Para verificar se o git está instalado, digite git --version
no terminal ou na linha de comando e verifique se ele é executado corretamente.
git clone https://github.com/googlecodelabs/android-datastore
O estado inicial fica na ramificação master
. O código da solução está na ramificação preferences_datastore
(link em inglês).
Se você não tiver o git, clique no botão abaixo para fazer o download de todo o código deste codelab:
Fazer o download do código-fonte
- Descompacte o código e abra o projeto no Android Studio Arctic Fox.
- Execute a configuração de execução do app em um dispositivo ou emulador.
O app vai ser executado e exibirá a lista de tarefas:
3. Visão geral do projeto
O app permite que você veja uma lista de tarefas. Cada tarefa tem as seguintes propriedades: nome, status concluído, prioridade e prazo.
Para simplificar o código com que precisamos trabalhar, o app permite que você faça apenas duas ações:
- Ativar ou desativar a visibilidade de Mostrar tarefas concluídas: por padrão, as tarefas ficam ocultas
- Classificar as tarefas por prioridade, por prazo ou por prazo e prioridade
O app segue a arquitetura recomendada no Guia para a arquitetura do app. Veja o que você vai encontrar em cada pacote:
data
- A classe modelo
Task
. - Classe
TasksRepository
: responsável por fornecer as tarefas. Para simplificar, ele retorna dados fixados no código e os expõe usando umFlow
para representar um cenário mais realista. - A classe
UserPreferencesRepository
que contém aSortOrder
, definida como umaenum
. A ordem de classificação atual é salva em SharedPreferences como umaString
, com base no nome do valor da enumeração. Ela expõe métodos síncronos para salvar e acessar a ordem de classificação.
ui
- Classes relacionadas à exibição de uma
Activity
usando umaRecyclerView
. - A classe
TasksViewModel
é responsável pela lógica da IU.
O TasksViewModel
contém todos os elementos necessários para criar os dados que precisam ser exibidos na IU: a lista de tarefas, as sinalizações showCompleted
e sortOrder
, incluídas em um objeto TasksUiModel
. Toda vez que um desses valores mudar, vamos precisar criar um novo TasksUiModel
. Para isso, combinamos três elementos:
- Um
Flow<List<Task>>
extraído doTasksRepository
. - Um
MutableStateFlow<Boolean>
que contém a sinalizaçãoshowCompleted
mais recente, que é mantida apenas na memória. - Um
MutableStateFlow<SortOrder>
que contém o valor mais recente dasortOrder
.
Para garantir que estamos atualizando a IU corretamente, um LiveData<TasksUiModel>
vai ser exposto apenas quando a atividade for iniciada.
Temos alguns problemas com nosso código:
- Bloqueamos a linha de execução de IU na E/S do disco ao inicializar
UserPreferencesRepository.sortOrder
. Isso pode resultar em instabilidade da IU. - A sinalização
showCompleted
é mantida somente na memória. Portanto, ela vai ser redefinida sempre que o usuário abrir o app. Assim como asortOrder
, ela precisa sobreviver ao fechamento do app. - Estamos usando SharedPreferences para persistir os dados, mas um
MutableStateFlow
, que modificamos manualmente, é mantido na memória para podermos receber notificações de mudanças. Esse processo pode ser facilmente corrompido se o valor for modificado em algum outro lugar do aplicativo. - Em
UserPreferencesRepository
, disponibilizamos dois métodos para atualizar a ordem de classificação:enableSortByDeadline()
eenableSortByPriority()
. Esses dois métodos dependem do valor da ordem de classificação atual. No entanto, se um for chamado antes do outro terminar, o resultado vai ser o valor final errado. Além disso, esses métodos podem resultar em instabilidades na IU e violações no modo restrito à medida que são chamados na linha de execução de IU.
Vamos ver como usar o DataStore para nos ajudar com esses problemas.
4. DataStore: noções básicas
Muitas vezes, você pode precisar armazenar conjuntos de dados pequenos ou simples. Você pode ter usado SharedPreferences antes para fazer isso, mas essa API também tem várias desvantagens. O objetivo da biblioteca Jetpack DataStore é resolver esses problemas com a criação de uma API simples, segura e assíncrona para armazenar dados. Ela oferece duas implementações diferentes:
- Preferences DataStore
- Proto DataStore
Recurso | SharedPreferences | PreferencesDataStore | ProtoDataStore |
API Async | ✅ (apenas para ler valores alterados, usando um listener) | ✅ (via | ✅ (via |
API Synchronous | ✅ (mas não é seguro para chamadas na linha de execução de IU) | ❌ | ❌ |
É seguro para chamadas na linha de execução de IU | ❌1 | ✅ (o trabalho é movido internamente para | ✅ (o trabalho é movido internamente para |
Pode sinalizar erros | ❌ | ✅ | ✅ |
Protegido contra exceções de execução | ❌2 | ✅ | ✅ |
Tem uma API transacional com garantias de consistência forte | ❌ | ✅ | ✅ |
Gerencia a migração de dados | ❌ | ✅ | ✅ |
Segurança de tipo | ❌ | ❌ | ✅ com buffers de protocolo |
1 SharedPreferences tem uma API síncrona que pode parecer segura para chamadas na linha de execução de IU, mas que realiza operações de E/S de disco. Além disso, apply()
bloqueia a linha de execução de IU em fsync()
. Chamadas fsync()
pendentes vão ser acionadas sempre que um serviço for iniciado ou interrompido e toda vez que uma atividade for iniciada ou interrompida em qualquer lugar do aplicativo. A linha de execução de IU é bloqueada durante chamadas fsync()
pendentes programadas por apply()
, geralmente sendo uma fonte de erros ANRs.
2 SharedPreferences gera erros de análise como exceções de tempo de execução.
Preferences x Proto DataStore
Embora tanto o Preferences quanto o Proto DataStore permitam salvar dados, eles fazem isso de maneiras diferentes:
- O Preference DataStore, assim como SharedPreferences, acessa dados com base em chaves, sem definir um esquema antecipadamente.
- O Proto DataStore define o esquema usando buffers de protocolo. Usar Protobufs permite manter dados fortemente categorizados. Eles são mais rápidos, menores, simples e menos ambíguos que XML e outros formatos de dados semelhantes. Embora o Proto DataStore exija que você aprenda um novo mecanismo de serialização, acreditamos que a vantagem de categorização fornecida por ele valha a pena.
Room x DataStore
Se você precisa de atualizações parciais, integridade referencial ou conjuntos de dados grandes/complexos, use o Room em vez do DataStore. O DataStore é ideal para conjuntos de dados pequenos e simples e não oferece suporte a atualizações parciais ou integridade referencial.
5. Visão geral do Preferences DataStore
A API Preferences DataStore é semelhante à SharedPreferences, mas tem várias diferenças importantes:
- Gerenciamento de atualizações de dados de maneira transacional.
- Exposição de um fluxo representando o estado atual dos dados.
- Não tem métodos de persistência de dados (
apply()
,commit()
). - Não retorna referências mutáveis para seu estado interno.
- Exposição de uma API semelhante a
Map
eMutableMap
com chaves digitadas.
Vamos ver como adicioná-lo ao projeto e migrar SharedPreferences para o DataStore.
Como adicionar dependências
Atualize o arquivo build.gradle para adicionar esta dependência do Preference DataStore:
implementation "androidx.datastore:datastore-preferences:1.0.0"
6. Como persistir dados no Preferences DataStore
Embora as sinalizações showCompleted
e sortOrder
sejam mostradas para o usuário, elas são representadas como dois objetos diferentes. Uma das nossas metas é unificar as duas sinalizações em uma classe UserPreferences
e a armazenar em UserPreferencesRepository
usando o DataStore. No momento, a sinalização showCompleted
é mantida na memória no TasksViewModel
.
Vamos começar criando uma classe de dados UserPreferences
em UserPreferencesRepository
. Por enquanto, ela precisa ter apenas um campo: showCompleted
. Adicionaremos a ordem de classificação mais tarde.
data class UserPreferences(val showCompleted: Boolean)
Como criar o DataStore
Para criar uma instância DataStore, usamos a delegação preferencesDataStore
com o Context
como receptor. Para simplificar, neste codelab, vamos fazer isso em TasksActivity
:
private const val USER_PREFERENCES_NAME = "user_preferences"
private val Context.dataStore by preferencesDataStore(
name = USER_PREFERENCES_NAME
)
A delegação preferencesDataStore
garante que tenhamos uma única instância do DataStore com esse nome no aplicativo. Atualmente, UserPreferencesRepository
é implementado como um Singleton, porque ele contém o sortOrderFlow
e evita que ele seja vinculado ao ciclo de vida da TasksActivity
. Como o UserPreferenceRepository
funciona somente com os dados do DataStore e não cria ou contém nenhum objeto novo, já podemos remover a implementação do Singleton:
- Remova o
companion object
. - Torne o
constructor
público.
O UserPreferencesRepository
precisa receber uma instância DataStore
como parâmetro de construtor. Por enquanto, podemos deixar Context
como um parâmetro por ser necessário para o SharedPreferences, mas ele vai ser removido mais tarde.
class UserPreferencesRepository(
private val userPreferencesStore: DataStore<UserPreferences>,
context: Context
) { ... }
Vamos atualizar a construção do UserPreferencesRepository
na TasksActivity
e transmitir o dataStore
:
viewModel = ViewModelProvider(
this,
TasksViewModelFactory(
TasksRepository,
UserPreferencesRepository(dataStore, this)
)
).get(TasksViewModel::class.java)
Como ler dados do Preferences DataStore
O Preferences DataStore expõe os dados armazenados em um Flow<Preferences>
que será emitido sempre que uma preferência mudar. Não queremos expor todo o objeto Preferences
e sim o objeto UserPreferences
. Para fazer isso, vamos mapear o Flow<Preferences>
, buscar o valor booleano que interessa a você, com base em uma chave e construir um objeto UserPreferences
.
Portanto, a primeira coisa que precisamos fazer é definir a chave show_completed
. Esse é um valor booleanPreferencesKey
que podemos declarar como membro em um objeto PreferencesKeys
particular.
private object PreferencesKeys {
val SHOW_COMPLETED = booleanPreferencesKey("show_completed")
}
Vamos expor um userPreferencesFlow: Flow<UserPreferences>
, construído com base em dataStore.data: Flow<Preferences>
, que será mapeado para recuperar a preferência certa:
val userPreferencesFlow: Flow<UserPreferences> = dataStore.data
.map { preferences ->
// Get our show completed value, defaulting to false if not set:
val showCompleted = preferences[PreferencesKeys.SHOW_COMPLETED]?: false
UserPreferences(showCompleted)
}
Como gerenciar exceções durante a leitura de dados
Conforme o DataStore lê dados de um arquivo, IOExceptions
são gerados quando ocorre um erro de leitura. Podemos lidar com isso usando o operador de fluxo catch()
antes de map()
e emitindo emptyPreferences()
caso a exceção gerada seja IOException
. Se um tipo diferente de exceção for gerada, prefira gerá-la novamente.
val userPreferencesFlow: Flow<UserPreferences> = dataStore.data
.catch { exception ->
// dataStore.data throws an IOException when an error is encountered when reading data
if (exception is IOException) {
emit(emptyPreferences())
} else {
throw exception
}
}.map { preferences ->
// Get our show completed value, defaulting to false if not set:
val showCompleted = preferences[PreferencesKeys.SHOW_COMPLETED]?: false
UserPreferences(showCompleted)
}
Como gravar dados no Preferences DataStore
Para gravar dados, o DataStore oferece uma função DataStore.edit(transform: suspend (MutablePreferences) -> Unit)
de suspensão, que aceita um bloco transform
que nos permite atualizar de maneira transacional o estado no DataStore.
As MutablePreferences
transmitidas para o bloco de transformação serão atualizadas com as edições executadas anteriormente. Todas as mudanças em MutablePreferences
no bloco transform
serão aplicadas ao disco após transform
ser concluído e antes de edit
ser concluído. Definir um valor em MutablePreferences
não afetará as outras preferências.
Observação: não tente modificar MutablePreferences
fora do bloco de transformação.
Vamos criar uma função de suspensão que nos permita atualizar a propriedade showCompleted
de UserPreferences
, nomeada updateShowCompleted()
, que chama dataStore.edit()
e define o novo valor:
suspend fun updateShowCompleted(showCompleted: Boolean) {
dataStore.edit { preferences ->
preferences[PreferencesKeys.SHOW_COMPLETED] = showCompleted
}
}
edit()
pode gerar uma IOException
se um erro for encontrado durante a leitura ou gravação no disco. Se qualquer outro erro ocorrer no bloco de transformação, ele será gerado por edit()
.
Nesse ponto, o app vai ser compilado, mas a funcionalidade que acabamos de criar em UserPreferencesRepository
não é usada.
7. SharedPreferences para Preferences DataStore
A ordem de classificação é salva em SharedPreferences. Vamos transferir para o DataStore. Para fazer isso, vamos começar atualizando UserPreferences
para também armazenar a ordem de classificação:
data class UserPreferences(
val showCompleted: Boolean,
val sortOrder: SortOrder
)
Como migrar de SharedPreferences
A fim de migrar para o DataStore, precisamos atualizar o builder do DataStore e transmitir uma SharedPreferencesMigration
à lista de migrações. O DataStore vai poder migrar automaticamente de SharedPreferences
para o DataStore. As migrações são executadas antes de qualquer dado ser acessado no DataStore. Isso significa que a migração precisa ser bem-sucedida antes de DataStore.data
emitir valores e antes que DataStore.edit()
possa atualizar os dados.
Observação: as chaves só são migradas de SharedPreferences uma vez. Portanto, é necessário parar de usar o SharedPreferences antigo após o código ser migrado para o DataStore.
Primeiro, vamos atualizar a criação do DataStore na TasksActivity
:
private const val USER_PREFERENCES_NAME = "user_preferences"
private val Context.dataStore by preferencesDataStore(
name = USER_PREFERENCES_NAME,
produceMigrations = { context ->
// Since we're migrating from SharedPreferences, add a migration based on the
// SharedPreferences name
listOf(SharedPreferencesMigration(context, USER_PREFERENCES_NAME))
}
)
Em seguida, adicione sort_order
ao PreferencesKeys
:
private object PreferencesKeys {
...
// Note: this has the the same name that we used with SharedPreferences.
val SORT_ORDER = stringPreferencesKey("sort_order")
}
Todas as chaves vão ser migradas para o nosso DataStore e excluídas das preferências do usuário em SharedPreferences. Agora, usando Preferences
será possível conseguir e atualizar a SortOrder
com base na chave SORT_ORDER
.
Como ler a ordem de classificação do DataStore
Atualize o userPreferencesFlow
para recuperar a ordem de classificação na transformação map()
:
val userPreferencesFlow: Flow<UserPreferences> = dataStore.data
.catch { exception ->
if (exception is IOException) {
emit(emptyPreferences())
} else {
throw exception
}
}.map { preferences ->
// Get the sort order from preferences and convert it to a [SortOrder] object
val sortOrder =
SortOrder.valueOf(
preferences[PreferencesKeys.SORT_ORDER] ?: SortOrder.NONE.name)
// Get our show completed value, defaulting to false if not set:
val showCompleted = preferences[PreferencesKeys.SHOW_COMPLETED] ?: false
UserPreferences(showCompleted, sortOrder)
}
Como salvar a ordem de classificação no DataStore
Atualmente, UserPreferencesRepository
expõe apenas uma maneira síncrona de definir a sinalização da ordem de classificação e tem um problema de simultaneidade. Expomos dois métodos para atualizar a ordem de classificação: enableSortByDeadline()
e enableSortByPriority()
. Esses dois métodos dependem do valor da ordem de classificação atual. No entanto, se um for chamado antes do outro terminar, o valor final estará errado.
Como o DataStore garante que as atualizações de dados aconteçam de maneira transacional, não teremos mais esse problema. Faça as seguintes mudanças:
- Atualize
enableSortByDeadline()
eenableSortByPriority()
para que sejam funçõessuspend
que usam odataStore.edit()
. - No bloco de transformação de
edit()
, receberemoscurrentOrder
do parâmetro Preferences, em vez de recuperá-lo do campo_sortOrderFlow
. - Em vez de chamar
updateSortOrder(newSortOrder)
, é possível atualizar diretamente a ordem de classificação nas preferências.
Veja como é a implementação.
suspend fun enableSortByDeadline(enable: Boolean) {
// edit handles data transactionally, ensuring that if the sort is updated at the same
// time from another thread, we won't have conflicts
dataStore.edit { preferences ->
// Get the current SortOrder as an enum
val currentOrder = SortOrder.valueOf(
preferences[PreferencesKeys.SORT_ORDER] ?: SortOrder.NONE.name
)
val newSortOrder =
if (enable) {
if (currentOrder == SortOrder.BY_PRIORITY) {
SortOrder.BY_DEADLINE_AND_PRIORITY
} else {
SortOrder.BY_DEADLINE
}
} else {
if (currentOrder == SortOrder.BY_DEADLINE_AND_PRIORITY) {
SortOrder.BY_PRIORITY
} else {
SortOrder.NONE
}
}
preferences[PreferencesKeys.SORT_ORDER] = newSortOrder.name
}
}
suspend fun enableSortByPriority(enable: Boolean) {
// edit handles data transactionally, ensuring that if the sort is updated at the same
// time from another thread, we won't have conflicts
dataStore.edit { preferences ->
// Get the current SortOrder as an enum
val currentOrder = SortOrder.valueOf(
preferences[PreferencesKeys.SORT_ORDER] ?: SortOrder.NONE.name
)
val newSortOrder =
if (enable) {
if (currentOrder == SortOrder.BY_DEADLINE) {
SortOrder.BY_DEADLINE_AND_PRIORITY
} else {
SortOrder.BY_PRIORITY
}
} else {
if (currentOrder == SortOrder.BY_DEADLINE_AND_PRIORITY) {
SortOrder.BY_DEADLINE
} else {
SortOrder.NONE
}
}
preferences[PreferencesKeys.SORT_ORDER] = newSortOrder.name
}
}
Agora é possível remover o parâmetro de construtor context
e todos os usos de SharedPreferences.
8. Atualizar TasksViewModel para usar UserPreferencesRepository
Agora que o UserPreferencesRepository
armazena as sinalizações show_completed
e sort_order
no DataStore e expõe um Flow<UserPreferences>
, vamos atualizar o TasksViewModel
para usar esses elementos.
Remova showCompletedFlow
e sortOrderFlow
. Crie um valor com o nome userPreferencesFlow
que vai ser inicializado com userPreferencesRepository.userPreferencesFlow
:
private val userPreferencesFlow = userPreferencesRepository.userPreferencesFlow
Na criação do tasksUiModelFlow
, substitua showCompletedFlow
e sortOrderFlow
por userPreferencesFlow
. Substitua os parâmetros corretamente.
Ao chamar filterSortTasks
, transmita showCompleted
e sortOrder
das userPreferences
. O código ficará assim:
private val tasksUiModelFlow = combine(
repository.tasks,
userPreferencesFlow
) { tasks: List<Task>, userPreferences: UserPreferences ->
return@combine TasksUiModel(
tasks = filterSortTasks(
tasks,
userPreferences.showCompleted,
userPreferences.sortOrder
),
showCompleted = userPreferences.showCompleted,
sortOrder = userPreferences.sortOrder
)
}
A função showCompletedTasks()
agora precisa ser atualizada para chamar userPreferencesRepository.updateShowCompleted()
. Como esta é uma função de suspensão, crie uma nova corrotina no viewModelScope
:
fun showCompletedTasks(show: Boolean) {
viewModelScope.launch {
userPreferencesRepository.updateShowCompleted(show)
}
}
As funções de userPreferencesRepository
, enableSortByDeadline()
e enableSortByPriority()
agora são de suspensão. Portanto, também precisam ser chamadas em uma nova corrotina, iniciada no viewModelScope
:
fun enableSortByDeadline(enable: Boolean) {
viewModelScope.launch {
userPreferencesRepository.enableSortByDeadline(enable)
}
}
fun enableSortByPriority(enable: Boolean) {
viewModelScope.launch {
userPreferencesRepository.enableSortByPriority(enable)
}
}
Limpar UserPreferencesRepository
Vamos remover os campos e métodos que não são mais necessários. Você poderá excluir o seguinte:
_sortOrderFlow
sortOrderFlow
updateSortOrder()
private val sortOrder: SortOrder
Agora, o app vai ser compilado corretamente. Execute o app para ver se as sinalizações show_completed
e sort_order
foram salvas corretamente.
Confira a ramificação preferences_datastore
do repositório do codelab para comparar as mudanças.
9. Resumo
Agora que você migrou para o Preferences DataStore, vamos recapitular o que aprendemos:
- SharedPreferences vem com uma série de desvantagens. Uma API síncrona que pode parecer segura para chamadas na linha de execução de IU, nenhum mecanismo de sinalização de erros, a falta da API transacional, entre outras.
- O DataStore é um substituto de SharedPreferences, sendo a solução para a maioria das limitações da API.
- O DataStore tem uma API totalmente assíncrona que usa corrotinas e fluxos Kotlin, gerencia a migração de dados, garante a consistência de dados e lida com casos de corrupção deles.