1. Antes de começar
Introdução
Nesta unidade, você aprendeu a usar o SQL e o Room para salvar dados localmente em um dispositivo. Essas ferramentas são avançadas. No entanto, nos casos em que você não precisa armazenar dados relacionais, o DataStore pode oferecer uma solução simples. O componente Jetpack DataStore é uma ótima maneira de armazenar conjuntos de dados pequenos e simples com baixa sobrecarga. O DataStore tem duas implementações diferentes: Preferences DataStore
e Proto DataStore
.
- O
Preferences DataStore
armazena pares de chave-valor. Os valores podem ser tipos de dados básicos do Kotlin, comoString
,Boolean
eInteger
. Ele não armazena conjuntos de dados complexos. Ele não requer um esquema predefinido. O principal caso de uso doPreferences Datastore
é armazenar preferências do usuário no dispositivo. - O
Proto DataStore
armazena tipos de dados personalizados. Ele exige um esquema predefinido que mapeie definições proto com estruturas de objetos.
Este codelab aborda apenas o Preferences DataStore
, mas você pode ler mais sobre o Proto DataStore
na documentação do DataStore.
O Preferences DataStore
é uma ótima maneira de armazenar configurações controladas pelo usuário. Neste codelab, você vai aprender a implementar o DataStore
para fazer exatamente isso.
Pré-requisitos:
- Concluir o curso "Noções básicas do Android com o Compose" pelo codelab Ler e atualizar dados com o Room.
O que é necessário
- Um computador com acesso à Internet e o Android Studio.
- Um dispositivo ou emulador.
- O código inicial do app Dessert Release.
O que você vai criar
O app Dessert Release mostra uma lista de versões do Android. O ícone na barra de apps alterna o layout entre uma visualização em grade e uma em lista.
No estado atual, o app não salva a seleção do layout. Quando você fechar o app, a seleção do layout não vai ser salva, e a configuração voltará à seleção padrão. Neste codelab, você vai adicionar um DataStore
ao app Dessert Release e usá-lo para armazenar uma preferência de seleção de layout.
2. Faça o download do código inicial
Clique no link abaixo para fazer o download de todo o código para este codelab:
Ou, se preferir, clone o código da versão do Dessert no GitHub:
$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-dessert-release.git $ cd basic-android-kotlin-compose-training-dessert-release $ git checkout starter
- No Android Studio, abra a pasta
basic-android-kotlin-compose-training-dessert-release
. - Abra o código do app Dessert Release no Android Studio.
3. Configurar dependências
Adicione o código abaixo a dependencies
no arquivo app/build.gradle.kts
:
implementation("androidx.datastore:datastore-preferences:1.0.0")
4. Implementar o repositório de preferências do usuário
- No pacote
data
, crie uma nova classe com o nomeUserPreferencesRepository
.
- No construtor
UserPreferencesRepository
, defina uma propriedade de valor particular para representar uma instância de objetoDataStore
com um tipoPreferences
.
class UserPreferencesRepository(
private val dataStore: DataStore<Preferences>
){
}
O DataStore
armazena pares de chave-valor. Para acessar um valor, você precisa definir uma chave.
- Crie um
companion object
dentro da classeUserPreferencesRepository
. - Use a função
booleanPreferencesKey()
para definir uma chave e transmitir o nomeis_linear_layout
a ela. Semelhante aos nomes de tabelas SQL, a chave precisa usar um formato de sublinhado. Essa chave é usada para acessar um valor booleano que indica se o layout linear precisa ser mostrado.
class UserPreferencesRepository(
private val dataStore: DataStore<Preferences>
){
private companion object {
val IS_LINEAR_LAYOUT = booleanPreferencesKey("is_linear_layout")
}
...
}
Gravar no DataStore
Você cria e modifica os valores em um DataStore
transmitindo uma lambda para o método edit()
. A lambda recebe uma instância de MutablePreferences
, que pode ser usada para atualizar valores no DataStore
. Todas as atualizações dentro dessa lambda são executadas como uma única transação. Ou seja, a atualização é atômica: ela acontece de uma só vez. Esse tipo de atualização evita situações em que alguns valores são atualizados, mas outros não.
- Crie uma função de suspensão com o nome
saveLayoutPreference()
. - Na função
saveLayoutPreference()
, chame o métodoedit()
no objetodataStore
.
suspend fun saveLayoutPreference(isLinearLayout: Boolean) {
dataStore.edit {
}
}
- Para que o código fique mais legível, defina um nome para as
MutablePreferences
fornecidas no corpo da lambda. Use essa propriedade para definir um valor com a chave definida e o booleano transmitido para a funçãosaveLayoutPreference()
.
suspend fun saveLayoutPreference(isLinearLayout: Boolean) {
dataStore.edit { preferences ->
preferences[IS_LINEAR_LAYOUT] = isLinearLayout
}
}
Ler do DataStore
Agora que você criou uma maneira de gravar isLinearLayout
no dataStore
, siga estas etapas:
- Crie uma propriedade no
UserPreferencesRepository
do tipoFlow<Boolean>
e com o nomeisLinearLayout
.
val isLinearLayout: Flow<Boolean> =
- É possível usar a propriedade
DataStore.data
para expor valoresDataStore
. DefinaisLinearLayout
como a propriedadedata
do objetoDataStore
.
val isLinearLayout: Flow<Boolean> = dataStore.data
A propriedade data
é um Flow
de objetos Preferences
. Os objetos Preferences
contêm todos os pares de chave-valor no DataStore. Sempre que os dados no DataStore são atualizados, um novo objeto Preferences
é emitido no Flow
.
- Use a função "map" para converter o
Flow<Preferences>
em umFlow<Boolean>
.
Essa função aceita uma lambda com o objeto Preferences
atual como parâmetro. Você pode especificar a chave definida anteriormente para acessar a preferência de layout. Não esqueça que o valor pode não existir se saveLayoutPreference
ainda não tiver sido chamado. Portanto, também é necessário fornecer um valor padrão.
- Especifique
true
para adotar a visualização de layout linear por padrão.
val isLinearLayout: Flow<Boolean> = dataStore.data.map { preferences ->
preferences[IS_LINEAR_LAYOUT] ?: true
}
Como processar exceções
Sempre que você interage com o sistema de arquivos em um dispositivo, é possível que algo falhe. Por exemplo, um arquivo pode não existir ou o disco pode estar cheio ou desconectado. Como o DataStore
lê e grava dados de arquivos, podem ocorrer IOExceptions
ao acessar o DataStore
. Use o operador catch{}
para capturar exceções e lidar com essas falhas.
- No objeto complementar, implemente uma propriedade de string
TAG
imutável que será usada para gerar registros.
private companion object {
val IS_LINEAR_LAYOUT = booleanPreferencesKey("is_linear_layout")
const val TAG = "UserPreferencesRepo"
}
- O
Preferences DataStore
gera umaIOException
quando um erro é encontrado durante a leitura de dados. No bloco de inicializaçãoisLinearLayout
, antes demap()
, use o operadorcatch{}
para capturar aIOException
.
val isLinearLayout: Flow<Boolean> = dataStore.data
.catch {}
.map { preferences ->
preferences[IS_LINEAR_LAYOUT] ?: true
}
- No bloco "catch", se houver uma
IOexception
, registre o erro e emitaemptyPreferences()
. Se um tipo diferente de exceção for gerado, prefira gerá-la novamente. Ao emitiremptyPreferences()
caso haja um erro, a função "map" ainda poderá mapear para o valor padrão.
val isLinearLayout: Flow<Boolean> = dataStore.data
.catch {
if(it is IOException) {
Log.e(TAG, "Error reading preferences.", it)
emit(emptyPreferences())
} else {
throw it
}
}
.map { preferences ->
preferences[IS_LINEAR_LAYOUT] ?: true
}
5. Inicializar o DataStore
Neste codelab, você vai precisar processar a injeção de dependências manualmente. Portanto, forneça um Preferences DataStore
manualmente à classe UserPreferencesRepository
. Siga estas etapas para injetar o DataStore
no UserPreferencesRepository
.
- Localize o pacote
dessertrelease
. - Nesse diretório, crie uma nova classe com o nome
DessertReleaseApplication
e implemente a classeApplication
. Este é o contêiner do seu DataStore.
class DessertReleaseApplication: Application() {
}
- No arquivo
DessertReleaseApplication.kt
, mas fora da classeDessertReleaseApplication
, declare um valorprivate const val
com o nomeLAYOUT_PREFERENCE_NAME
. - Atribua à variável
LAYOUT_PREFERENCE_NAME
o valor de stringlayout_preferences
, que pode ser usado como o nome doPreferences Datastore
que você vai instanciar na próxima etapa.
private const val LAYOUT_PREFERENCE_NAME = "layout_preferences"
- Ainda fora do corpo da classe
DessertReleaseApplication
, mas no arquivoDessertReleaseApplication.kt
, crie uma propriedade de valor particular do tipoDataStore<Preferences>
com o nomeContext.dataStore
usando o delegadopreferencesDataStore
. TransmitaLAYOUT_PREFERENCE_NAME
ao parâmetroname
do delegadopreferencesDataStore
.
private const val LAYOUT_PREFERENCE_NAME = "layout_preferences"
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
name = LAYOUT_PREFERENCE_NAME
)
- No corpo da classe
DessertReleaseApplication
, crie uma instâncialateinit var
doUserPreferencesRepository
.
private const val LAYOUT_PREFERENCE_NAME = "layout_preferences"
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
name = LAYOUT_PREFERENCE_NAME
)
class DessertReleaseApplication: Application() {
lateinit var userPreferencesRepository: UserPreferencesRepository
}
- Modifique o método
onCreate()
.
private const val LAYOUT_PREFERENCE_NAME = "layout_preferences"
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
name = LAYOUT_PREFERENCE_NAME
)
class DessertReleaseApplication: Application() {
lateinit var userPreferencesRepository: UserPreferencesRepository
override fun onCreate() {
super.onCreate()
}
}
- No método
onCreate()
, inicializeuserPreferencesRepository
criando umUserPreferencesRepository
com odataStore
como parâmetro.
private const val LAYOUT_PREFERENCE_NAME = "layout_preferences"
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
name = LAYOUT_PREFERENCE_NAME
)
class DessertReleaseApplication: Application() {
lateinit var userPreferencesRepository: UserPreferencesRepository
override fun onCreate() {
super.onCreate()
userPreferencesRepository = UserPreferencesRepository(dataStore)
}
}
- Adicione a linha mostrada abaixo à tag
<application>
no arquivoAndroidManifest.xml
.
<application
android:name=".DessertReleaseApplication"
...
</application>
Essa abordagem define a classe DessertReleaseApplication
como o ponto de entrada do app. O objetivo desse código é inicializar as dependências definidas na classe DessertReleaseApplication
antes de iniciar a MainActivity
.
6. Usar o UserPreferencesRepository
Fornecer o repositório ao ViewModel
Agora que o UserPreferencesRepository
está disponível por injeção de dependência, você pode usá-lo no DessertReleaseViewModel
.
- No
DessertReleaseViewModel
, crie uma propriedadeUserPreferencesRepository
como um parâmetro construtor.
class DessertReleaseViewModel(
private val userPreferencesRepository: UserPreferencesRepository
) : ViewModel() {
...
}
- No objeto complementar do
ViewModel
, no blocoviewModelFactory initializer
, receba uma instância doDessertReleaseApplication
usando o código abaixo.
...
companion object {
val Factory: ViewModelProvider.Factory = viewModelFactory {
initializer {
val application = (this[APPLICATION_KEY] as DessertReleaseApplication)
...
}
}
}
}
- Crie uma instância do
DessertReleaseViewModel
e transmita ouserPreferencesRepository
.
...
companion object {
val Factory: ViewModelProvider.Factory = viewModelFactory {
initializer {
val application = (this[APPLICATION_KEY] as DessertReleaseApplication)
DessertReleaseViewModel(application.userPreferencesRepository)
}
}
}
}
O UserPreferencesRepository
agora pode ser acessado pelo ViewModel. As próximas etapas envolvem usar os recursos de leitura e gravação do UserPreferencesRepository
que você implementou anteriormente.
Armazenar a preferência de layout
- Edite a função
selectLayout()
noDessertReleaseViewModel
para acessar o repositório de preferências e atualizar a preferência de layout. - A gravação no
DataStore
é feita de forma assíncrona com uma funçãosuspend
. Inicie uma nova corrotina para chamar a funçãosaveLayoutPreference()
do repositório de preferências.
fun selectLayout(isLinearLayout: Boolean) {
viewModelScope.launch {
userPreferencesRepository.saveLayoutPreference(isLinearLayout)
}
}
Ler a preferência de layout
Nesta seção, você vai refatorar o uiState: StateFlow
existente no ViewModel
para refletir o isLinearLayout: Flow
do repositório.
- Exclua o código que inicializa a propriedade
uiState
comoMutableStateFlow(DessertReleaseUiState)
.
val uiState: StateFlow<DessertReleaseUiState> =
A preferência de layout linear do repositório tem dois valores possíveis, verdadeiro ou falso, na forma de um Flow<Boolean>
. Esse valor precisa ser mapeado para um estado da interface.
- Defina o
StateFlow
como o resultado da transformação de coleçãomap()
, chamada emisLinearLayout Flow
.
val uiState: StateFlow<DessertReleaseUiState> =
userPreferencesRepository.isLinearLayout.map { isLinearLayout ->
}
- Retorne uma instância da classe de dados
DessertReleaseUiState
, transmitindoisLinearLayout Boolean
. A tela usa esse estado da interface para determinar as strings e os ícones corretos a serem mostrados.
val uiState: StateFlow<DessertReleaseUiState> =
userPreferencesRepository.isLinearLayout.map { isLinearLayout ->
DessertReleaseUiState(isLinearLayout)
}
O UserPreferencesRepository.isLinearLayout
é um Flow
que é frio. No entanto, para fornecer estados à interface, é melhor usar um fluxo quente, como StateFlow
, para que o estado esteja sempre disponível imediatamente.
- Use a função
stateIn()
para converter umFlow
em umStateFlow
. - A função
stateIn()
aceita três parâmetros:scope
,started
einitialValue
. TransmitaviewModelScope
,SharingStarted.WhileSubscribed(5_000)
eDessertReleaseUiState()
para esses parâmetros, respectivamente.
val uiState: StateFlow<DessertReleaseUiState> =
userPreferencesRepository.isLinearLayout.map { isLinearLayout ->
DessertReleaseUiState(isLinearLayout)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = DessertReleaseUiState()
)
- Inicie o app. É possível clicar no ícone para alternar entre um layout de grade e um layout linear.
Parabéns! Você adicionou o Preferences DataStore
ao app para salvar a preferência de layout do usuário.
7. Acessar o código da solução
Para baixar o código do codelab concluído, use estes comandos git:
$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-dessert-release.git $ cd basic-android-kotlin-compose-training-dessert-release $ git checkout main
Se preferir, você pode baixar o repositório como um arquivo ZIP, descompactar e abrir no Android Studio.
Se você quiser conferir o código da solução, acesse o GitHub (em inglês).