Preferences DataStore

1. Antes de começar

Nos codelabs anteriores, você aprendeu a salvar dados em um banco de dados SQLite usando o Room, que é uma camada de abstração sobre um banco de dados. Este codelab apresenta o Jetpack DataStore. Criado com base em corrotinas e fluxo Kotlin, o DataStore oferece duas implementações diferentes: o Proto DataStore, que armazena objetos tipados, e o Preferences DataStore, que armazena pares de chave-valor.

Neste codelab, você vai aprender a usar o Preferences DataStore. O Proto DataStore está além do escopo deste codelab.

Pré-requisitos

  • Ter familiaridade com os Componentes da arquitetura do Android ViewModel, LiveData e Flow e saber como usar ViewModelProvider.Factory para instanciar o ViewModel.
  • Ter familiaridade com os princípios básicos de simultaneidade.
  • Saber usar corrotinas para tarefas de longa duração.

O que você vai aprender

  • O que é o DataStore, por que e quando o usar.
  • Como adicionar o Preference DataStore ao app.

O que é necessário

  • O código inicial do app Words, que é o mesmo código da solução do app Words de um codelab anterior.
  • Um computador com o Android Studio instalado.

Fazer o download do código inicial para este codelab

Neste codelab, você vai ampliar os recursos do app Words criados com o código da solução anterior. O código inicial pode conter alguns códigos que você já viu em codelabs anteriores.

Para fazer o download do código deste codelab no GitHub e o abrir no Android Studio, siga as etapas abaixo.

  1. Inicie o Android Studio.
  2. Na janela Welcome to Android Studio clique em Check out project from version control.
  3. Escolha Git.

b89a22e2d8cf3b4e.png

  1. Na caixa de diálogo Clone Repository, cole o URL do código fornecido na caixa URL.
  2. Clique no botão Test, aguarde e veja se um balão pop-up verde é exibido, com a mensagem Connection successful.
  3. Você pode mudar o campo Directory para algo diferente do padrão sugerido, se preferir.

e4fb01c402e47bb3.png

  1. Clique em Clone. O Android Studio começará a buscar o código.
  2. No pop-up Checkout from Version Control, clique em Yes.

1902d34f29119530.png

  1. Aguarde o Android Studio abrir.
  2. Selecione o módulo correto para o código inicial ou da solução do codelab.

2371589274bce21c.png

  1. Clique no botão Run 11c34fc5e516fb1c.png para criar e executar o código.

2. Visão geral do app inicial

O app Words é composto por duas telas. A primeira tela mostra as letras que o usuário pode selecionar e a segunda exibe uma lista de palavras que começam com a letra selecionada.

O app tem uma opção de menu que permite que o usuário alterne entre os layouts de lista e de grade para exibir as letras.

  1. Faça o download do código inicial, abra-o no Android Studio e execute o app. As letras vão ser exibidas em um layout linear.
  2. Toque na opção de menu na parte de cima da tela, no canto direito. A tela vai mudar para o layout de grade.
  3. Saia e reinicie o app. Para isso, use as opções Stop 'app' 1c2b7a60ebd9a46e.png e Run 'app' 3b4c2b852ca05ab9.png no Android Studio. Ao reiniciar o app, as letras vão ser exibidas em um layout linear, e não de grade.

Observe que a preferência do usuário não foi mantida. Este codelab mostra como corrigir esse problema.

O que você vai criar

  • Neste codelab, você vai aprender a usar o Preferences DataStore para manter a configuração do layout no DataStore.

3. Introdução ao Preferences DataStore

O Preferences DataStore é ideal para conjuntos de dados pequenos e simples, como ao armazenar dados de login, configurações de modo escuro, tamanho da fonte, entre outros. O DataStore não é indicado para conjuntos de dados complexos, como uma lista de compras on-line ou um banco de dados de alunos. Se você precisar armazenar conjuntos de dados grandes ou complexos, use o Room em vez do DataStore.

Com a biblioteca Jetpack DataStore, é possível criar uma API simples, segura e assíncrona para armazenar dados. Essa biblioteca oferece duas implementações diferentes: o Preferences DataStore eCom a biblioteca Jetpack DataStore, é possível criar uma API simples, segura e assíncrona para armazenar dados. Essa biblioteca oferece duas implementações diferentes: o Preferences DataStore e

o Proto DataStore. Embora tanto o Preferences quanto o Proto DataStore permitam salvar dados, eles fazem isso de maneiras diferentes:

  • O Preferences DataStore acessa e armazena dados com base em chaves, sem definir um esquema antecipadamente, como um modelo de banco de dados.
  • O Proto DataStore define o esquema usando buffers de protocolo. O uso de buffers de protocolo, ou Protobufs, permite manter dados altamente tipados. Os Protobufs são mais rápidos, menores, mais simples e menos ambíguos que XML e outros formatos de dados semelhantes.

Room ou Datastore: quando usar

Caso seu app precise armazenar dados grandes ou complexos em um formato estruturado, como SQL, use o Room. No entanto, se você precisa armazenar apenas dados simples ou em pequenas quantidades, que possam ser salvos em pares de chave-valor, o DataStore é a escolha ideal.

Proto ou Preferences DataStore: quando usar

O Proto DataStore é eficiente e possui segurança de tipo, mas exige configuração. Se os dados do app forem simples o suficiente para serem salvos em pares de chave-valor, o Preferences DataStore é uma opção melhor, já que é muito mais fácil de configurar.

Adicionar o Preferences DataStore como uma dependência

O primeiro passo da integração de um DataStore a um app é adicioná-lo como uma dependência.

  1. Em build.gradle(Module: Words.app), adicione esta dependência.
implementation "androidx.datastore:datastore-preferences:1.0.0"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1"

4. Criar um Preferences DataStore

  1. Adicione um pacote com o nome data e crie uma classe Kotlin com o nome SettingsDataStore dentro do pacote.
  2. Adicione um parâmetro construtor à classe SettingsDataStore do tipo Context.
class SettingsDataStore(context: Context) {}
  1. Fora da classe SettingsDataStore, declare um private const val com o nome LAYOUT_PREFERENCES_NAME, e atribua o valor de string layout_preferences a ele. Esse é o nome do Preferences Datastore que você vai instanciar na próxima etapa.
private const val LAYOUT_PREFERENCES_NAME = "layout_preferences"
  1. Ainda fora da classe, crie uma instância DataStore usando o delegado preferencesDataStore. Como você está usando o Preferences Datastore, é necessário transmitir Preferences como um tipo de armazenamento de dados. Além disso, defina o name do DataStore como LAYOUT_PREFERENCES_NAME.

Veja abaixo o código concluído:

private const val LAYOUT_PREFERENCES_NAME = "layout_preferences"

// Create a DataStore instance using the preferencesDataStore delegate, with the Context as
// receiver.
private val Context.dataStore : DataStore<Preferences> by preferencesDataStore(
   name = LAYOUT_PREFERENCES_NAME
)

5. Implementar a classe SettingsDataStore

Como abordado, o Preferences DataStore armazena dados em pares de chave-valor. Nesta etapa, você vai definir as chaves necessárias para armazenar a configuração do layout e definir funções para gravar e ler dados no Preferences DataStore.

Funções de tipo de chave

O Preferences DataStore não usa um esquema predefinido, como o Room. Ao invés disso, ele usa funções de tipo de chave correspondentes a fim de definir uma chave para cada valor armazenado na instância DataStore<Preferences>. Por exemplo, a fim de definir uma chave para um valor int, use intPreferencesKey() e, para um valor string, use stringPreferencesKey(). Em geral, esses nomes de função têm como prefixo o tipo de dados que você quer armazenar com a chave.

Na classe data\SettingsDataStore, implemente o seguinte:

  1. Para implementar a classe SettingsDataStore, primeiro crie uma chave que armazena um valor booleano que especifica se a configuração do usuário é um layout linear. Crie uma propriedade de classe private com o nome IS_LINEAR_LAYOUT_MANAGER, e a inicialize usando booleanPreferencesKey(). Para isso, transmita o nome da chave is_linear_layout_manager como o parâmetro da função.
private val IS_LINEAR_LAYOUT_MANAGER = booleanPreferencesKey("is_linear_layout_manager")

Gravar no Preferences DataStore

Agora é hora de usar a chave e armazenar a configuração do layout booleano no DataStore. O Preferences DataStore disponibiliza uma função de suspensão edit() que atualiza os dados de forma transacional em DataStore. O parâmetro de transformação 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. Internamente, o trabalho de transação é movido ao Dispacter.IO. Portanto, não se esqueça de fazer com que a função seja suspend (suspensa) ao chamar a função edit().

  1. Crie uma função suspend com o nome saveLayoutToPreferencesStore(), que usa dois parâmetros: a configuração do layout booleano e o Context.
suspend fun saveLayoutToPreferencesStore(isLinearLayoutManager: Boolean, context: Context) {

}
  1. Implemente a função acima, chame dataStore.edit(), e transmita um bloco de código para armazenar o novo valor.
suspend fun saveLayoutToPreferencesStore(isLinearLayoutManager: Boolean, context: Context) {
   context.dataStore.edit { preferences ->
       preferences[IS_LINEAR_LAYOUT_MANAGER] = isLinearLayoutManager
   }
}

Ler do Preferences DataStore

O Preferences DataStore expõe os dados armazenados em um Flow<Preferences>, que é emitido sempre que uma preferência muda. O objetivo não é expor todo o objeto Preferences, e sim apenas o valor Boolean. Para fazer isso, é necessário mapear Flow<Preferences> para encontrar o valor do Boolean que será usado.

  1. Exponha um preferenceFlow: Flow<UserPreferences>, construído com base em dataStore.data: Flow<Preferences> e o mapeie para extrair a preferência Boolean. Como o DataStore está vazio na primeira execução, retorne true por padrão.
val preferenceFlow: Flow<Boolean> = context.dataStore.data
   .map { preferences ->
       // On the first run of the app, we will use LinearLayoutManager by default
       preferences[IS_LINEAR_LAYOUT_MANAGER] ?: true
   }
  1. Adicione as importações abaixo se elas não forem importadas automaticamente.
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map

Como processar exceções

Como o DataStore lê e grava dados de arquivos, podem ocorrer IOExceptions ao acessar os dados. Para as processar, use o operador catch() (link em inglês) a fim de capturar exceções.

  1. O SharedPreference DataStore gera uma IOException quando um erro é encontrado durante a leitura de dados. Na declaração preferenceFlow, antes do método map(), use o operador catch() (link em inglês) para capturar a IOException e emitir emptyPreferences(). De maneira mais simples, como não esperamos que ocorram outros tipos de exceção nesta etapa, se um tipo diferente for gerado, gere o elemento de exceção novamente.
val preferenceFlow: Flow<Boolean> = context.dataStore.data
   .catch {
       if (it is IOException) {
           it.printStackTrace()
           emit(emptyPreferences())
       } else {
           throw it
       }
   }
   .map { preferences ->
       // On the first run of the app, we will use LinearLayoutManager by default
       preferences[IS_LINEAR_LAYOUT_MANAGER] ?: true
   }

Agora a classe data\SettingsDataStore está pronta para ser usada.

6. Usar a classe SettingsDataStore

Na próxima tarefa, você usará o SettingsDataStore na classe LetterListFragment. Você vai anexar um observador à configuração do layout e atualizar a IU de acordo com a configuração.

Implemente as etapas abaixo no LetterListFragment:

  1. Declare uma variável de classe private com o nome SettingsDataStore do tipo SettingsDataStore. Defina essa variável como lateinit, porque ela será inicializada mais adiante.
private lateinit var SettingsDataStore: SettingsDataStore
  1. No final da função onViewCreated(), inicialize a nova variável e transmita requireContext() para o construtor do SettingsDataStore.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   ...
   // Initialize SettingsDataStore
   SettingsDataStore = SettingsDataStore(requireContext())
}

Ler e observar os dados

  1. No LetterListFragment, no método onViewCreated(), abaixo da inicialização SettingsDataStore, converta o preferenceFlow para Livedata usando asLiveData(). Anexe um observador e transmita o viewLifecycleOwner como proprietário.
SettingsDataStore.preferenceFlow.asLiveData().observe(viewLifecycleOwner, { })
  1. Dentro do observador, atribua a nova configuração de layout à variável isLinearLayoutManager. Chame a função chooseLayout() para atualizar o layout da RecyclerView.
SettingsDataStore.preferenceFlow.asLiveData().observe(viewLifecycleOwner, { value ->
           isLinearLayoutManager = value
           chooseLayout()
   })

Quando estiver concluída, a função onViewCreated() vai ficar assim:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   recyclerView = binding.recyclerView
   // Initialize SettingsDataStore
   SettingsDataStore = SettingsDataStore(requireContext())
   SettingsDataStore.preferenceFlow.asLiveData().observe(viewLifecycleOwner, { value ->
           isLinearLayoutManager = value
           chooseLayout()
   })
}

Gravar a configuração do layout no DataStore

A etapa final é gravar a configuração do layout no Preferences DataStore quando o usuário toca na opção do menu. A gravação de dados no Preference Datastore precisa ser realizada de forma assíncrona em uma corrotina. Para fazer isso em um fragmento, use o CoroutineScope (link em inglês), conhecido como LifecycleScope.

LifecycleScope

Componentes com reconhecimento de ciclo de vida, como fragmentos, oferecem suporte a corrotinas de primeira classe com escopos lógicos no app, além de uma camada de interoperabilidade com LiveData. Um LifecycleScope é definido para cada objeto Lifecycle. Toda corrotina iniciada nesse escopo vai ser cancelada quando o proprietário do Lifecycle for destruído.

  1. No LetterListFragment, na função onOptionsItemSelected(), no final do caso R.id.action_switch_layout, inicie a corrotina usando lifecycleScope. No bloco launch, faça uma chamada para o método saveLayoutToPreferencesStore(), transmitindo o isLinearLayoutManager e o context.
override fun onOptionsItemSelected(item: MenuItem): Boolean {
   return when (item.itemId) {
       R.id.action_switch_layout -> {
           ...
           // Launch a coroutine and write the layout setting in the preference Datastore
           lifecycleScope.launch {             SettingsDataStore.saveLayoutToPreferencesStore(isLinearLayoutManager, requireContext())
           }
           ...

           return true
       }
  1. Execute o app. Clique na opção de menu para mudar o layout do aplicativo.

  1. Agora, teste se o Preferences DataStore mantém as preferências selecionadas. Mude o layout do app para um layout de grade. Saia e reinicie o app. Para isso, use as opções Stop 'app' 1c2b7a60ebd9a46e.png e Run 'app' 3b4c2b852ca05ab9.png no Android Studio.

cd2c31f27dfb5157.png

Ao reiniciar o app, as letras vão ser exibidas em um layout de grade, e não linear. Isso significa que o app está salvando corretamente a configuração de layout selecionada pelo usuário.

Agora, as letras são exibidas em um layout de grade, mas o ícone de menu ainda não foi atualizado corretamente. Vamos corrigir esse problema em seguida.

7. Corrigir o bug no ícone de menu

A causa do bug no ícone de menu é que o layout da RecyclerView é atualizado de acordo com a configuração do layout no método onViewCreated(), mas o mesmo não acontece no ícone de menu. Esse problema pode ser resolvido reexibindo o menu durante a atualização do layout da RecyclerView.

Como reexibir o menu "opções"

Depois que um menu é criado, ele não precisa ser recriado em todos os frames, já que seria redundante fazer isso em cada frame. A função invalidateOptionsMenu() instrui o Android a reexibir o menu "opções".

Você pode chamar essa função quando algo mudar no menu "opções", como ao adicionar ou excluir um item do menu ou ao mudar o texto ou o ícone do menu. Nesse caso, o que muda é o ícone do menu. Chamar esse método declara que o menu "opções" mudou e, por isso, precisa ser recriado. O método onCreateOptionsMenu(android.view.Menu) é chamado da próxima vez em que o menu precisar ser exibido.

  1. No LetterListFragment, no método onViewCreated(), no final do observador preferenceFlow, abaixo da chamada para chooseLayout(). Recrie o menu chamando invalidateOptionsMenu() na activity.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   ...
   SettingsDataStore.preferenceFlow.asLiveData().observe(viewLifecycleOwner, { value ->
           ...
           // Redraw the menu
           activity?.invalidateOptionsMenu()
   })
}
  1. Execute o app novamente e mude o layout.
  2. Saia do app e o reinicie. Observe que agora o ícone do menu foi atualizado corretamente.

ce3474dba2a9c1c8.png

Parabéns! Você adicionou o Preferences DataStore ao app para salvar as preferências dos usuários.

8. Código da solução

O código da solução para este codelab está no projeto e no módulo mostrados abaixo.

9. Resumo

  • O DataStore tem uma API totalmente assíncrona que usa corrotinas e fluxo Kotlin, o que garante a consistência de dados.
  • O Jetpack DataStore é uma solução de armazenamento de dados que possibilita armazenar pares de chave-valor ou objetos tipados usando buffers de protocolo.
  • O DataStore oferece duas implementações diferentes: o Preferences DataStore e o Proto DataStore.
  • O Preferences DataStore não usa um esquema predefinido.
  • O Preferences DataStore usa a função de tipo de chave correspondente a fim de definir uma chave para cada valor que você precisa armazenar na instância DataStore<Preferences>. Por exemplo, a fim de definir uma chave para um valor de int, use intPreferencesKey().
  • O Preferences DataStore disponibiliza uma função edit(), que atualiza os dados de forma transacional em um DataStore.

10. Saiba mais

Blog

Dar preferência ao armazenamento de dados com o Jetpack DataStore (link em inglês)