Salvar preferências localmente com o DataStore

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, como String, Boolean e Integer. Ele não armazena conjuntos de dados complexos. Ele não requer um esquema predefinido. O principal caso de uso do Preferences 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:

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.

b6e4bd0e50915b81.png 24a261db4cf2c6b8.png

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
  1. No Android Studio, abra a pasta basic-android-kotlin-compose-training-dessert-release.
  2. 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

  1. No pacote data, crie uma nova classe com o nome UserPreferencesRepository.

c4c2e90902898001.png

  1. No construtor UserPreferencesRepository, defina uma propriedade de valor particular para representar uma instância de objeto DataStore com um tipo Preferences.
class UserPreferencesRepository(
    private val dataStore: DataStore<Preferences>
){
}

O DataStore armazena pares de chave-valor. Para acessar um valor, você precisa definir uma chave.

  1. Crie um companion object dentro da classe UserPreferencesRepository.
  2. Use a função booleanPreferencesKey() para definir uma chave e transmitir o nome is_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.

  1. Crie uma função de suspensão com o nome saveLayoutPreference().
  2. Na função saveLayoutPreference(), chame o método edit() no objeto dataStore.
suspend fun saveLayoutPreference(isLinearLayout: Boolean) {
    dataStore.edit {

    }
}
  1. 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ção saveLayoutPreference().
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:

  1. Crie uma propriedade no UserPreferencesRepository do tipo Flow<Boolean> e com o nome isLinearLayout.
val isLinearLayout: Flow<Boolean> =
  1. É possível usar a propriedade DataStore.data para expor valores DataStore. Defina isLinearLayout como a propriedade data do objeto DataStore.
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.

  1. Use a função "map" para converter o Flow<Preferences> em um Flow<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.

  1. 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.

  1. 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"
}
  1. O Preferences DataStore gera uma IOException quando um erro é encontrado durante a leitura de dados. No bloco de inicialização isLinearLayout, antes de map(), use o operador catch{} para capturar a IOException.
val isLinearLayout: Flow<Boolean> = dataStore.data
    .catch {}
    .map { preferences ->
        preferences[IS_LINEAR_LAYOUT] ?: true
    }
  1. No bloco "catch", se houver uma IOexception, registre o erro e emita emptyPreferences(). Se um tipo diferente de exceção for gerado, prefira gerá-la novamente. Ao emitir emptyPreferences() 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.

  1. Localize o pacote dessertrelease.
  2. Nesse diretório, crie uma nova classe com o nome DessertReleaseApplication e implemente a classe Application. Este é o contêiner do seu DataStore.
class DessertReleaseApplication: Application() {
}
  1. No arquivo DessertReleaseApplication.kt, mas fora da classe DessertReleaseApplication, declare um valor private const val com o nome LAYOUT_PREFERENCE_NAME.
  2. Atribua à variável LAYOUT_PREFERENCE_NAME o valor de string layout_preferences, que pode ser usado como o nome do Preferences Datastore que você vai instanciar na próxima etapa.
private const val LAYOUT_PREFERENCE_NAME = "layout_preferences"
  1. Ainda fora do corpo da classe DessertReleaseApplication, mas no arquivo DessertReleaseApplication.kt, crie uma propriedade de valor particular do tipo DataStore<Preferences> com o nome Context.dataStore usando o delegado preferencesDataStore. Transmita LAYOUT_PREFERENCE_NAME ao parâmetro name do delegado preferencesDataStore.
private const val LAYOUT_PREFERENCE_NAME = "layout_preferences"
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
    name = LAYOUT_PREFERENCE_NAME
)
  1. No corpo da classe DessertReleaseApplication, crie uma instância lateinit var do UserPreferencesRepository.
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
}
  1. 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()
    }
}
  1. No método onCreate(), inicialize userPreferencesRepository criando um UserPreferencesRepository com o dataStore 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)
    }
}
  1. Adicione a linha mostrada abaixo à tag <application> no arquivo AndroidManifest.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.

  1. No DessertReleaseViewModel, crie uma propriedade UserPreferencesRepository como um parâmetro construtor.
class DessertReleaseViewModel(
    private val userPreferencesRepository: UserPreferencesRepository
) : ViewModel() {
    ...
}
  1. No objeto complementar do ViewModel, no bloco viewModelFactory initializer, receba uma instância do DessertReleaseApplication usando o código abaixo.
    ...
    companion object {
        val Factory: ViewModelProvider.Factory = viewModelFactory {
            initializer {
                val application = (this[APPLICATION_KEY] as DessertReleaseApplication)
                ...
            }
        }
    }
}
  1. Crie uma instância do DessertReleaseViewModel e transmita o userPreferencesRepository.
    ...
    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

  1. Edite a função selectLayout() no DessertReleaseViewModel para acessar o repositório de preferências e atualizar a preferência de layout.
  2. A gravação no DataStore é feita de forma assíncrona com uma função suspend. Inicie uma nova corrotina para chamar a função saveLayoutPreference() 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.

  1. Exclua o código que inicializa a propriedade uiState como MutableStateFlow(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.

  1. Defina o StateFlow como o resultado da transformação de coleção map(), chamada em isLinearLayout Flow.
val uiState: StateFlow<DessertReleaseUiState> =
    userPreferencesRepository.isLinearLayout.map { isLinearLayout ->
}
  1. Retorne uma instância da classe de dados DessertReleaseUiState, transmitindo isLinearLayout 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.

  1. Use a função stateIn() para converter um Flow em um StateFlow.
  2. A função stateIn() aceita três parâmetros: scope, started e initialValue. Transmita viewModelScope, SharingStarted.WhileSubscribed(5_000) e DessertReleaseUiState() 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()
    )
  1. Inicie o app. É possível clicar no ícone para alternar entre um layout de grade e um layout linear.

b6e4bd0e50915b81.png 24a261db4cf2c6b8.png

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).