Como trabalhar com o Proto DataStore

1. Introdução

O que é o Datastore?

O DataStore é uma solução nova e melhorada de armazenamento de dados que visa substituir SharedPreferences. Criado com base em corrotinas e fluxo Kotlin, o DataStore oferece duas implementações diferentes: Proto DataStore, que permite armazenar objetos tipados (com suporte de buffers de protocolo) e Preferences DataStore, que armazena pares de valores-chave. 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 Proto DataStore.
  • Como migrar de SharedPreferences para o Proto 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.

fcb2ffa4e6b77f33.gif

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.

Como o DataStore tem duas implementações diferentes: Preferences DataStore e Proto DataStore, você vai aprender a usar o Proto DataStore seguindo estas tarefas em cada implementação:

  • Persistir o filtro de status concluído no DataStore.
  • Migrar a ordem de classificação de SharedPreferences para o DataStore.

Também recomendamos que você consulte o codelab Preferences DataStore para entender melhor a diferença entre os dois.

O que é necessário

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 proto_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

  1. Descompacte o código e abra o projeto no Android Studio Arctic Fox.
  2. Execute a configuração de execução do app em um dispositivo ou emulador.

b3c0dfdb92dfed77.png

O app vai ser executado e exibirá a lista de tarefas:

d3972939a2de88ba.png

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 um Flow para representar um cenário mais realista.
  • A classe UserPreferencesRepository que contém a SortOrder, definida como uma enum. A ordem de classificação atual é salva em SharedPreferences como uma String, 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 uma RecyclerView.
  • 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 do TasksRepository.
  • Um MutableStateFlow<Boolean> que contém a sinalização showCompleted mais recente, que é mantida apenas na memória.
  • Um MutableStateFlow<SortOrder> que contém o valor mais recente da sortOrder.

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 a SortOrder, 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() 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 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.

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 essas duas sinalizações em uma classe UserPreferences.

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 Flow e RxJava 2 e 3 Flowable)

✅ (via Flow e RxJava 2 e 3 Flowable)

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

✅ (o trabalho é movido internamente para Dispatchers.IO)

Pode sinalizar erros

Protegido contra exceções de tempo 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. Proto DataStore: visão geral

Um dos pontos negativos de SharedPreferences e do Preferences DataStore é que não há como definir um esquema ou garantir que as chaves sejam acessadas com o tipo correto. O Proto DataStore resolve esse problema usando buffers de protocolo para definir o esquema. Com o uso de arquivos proto, o DataStore sabe que tipos estão armazenados e somente os fornecerá, eliminando a necessidade de usar chaves.

Veja como adicionar o Proto DataStore e Protobufs ao projeto, o que são buffers de protocolo e como usá-los com o Proto DataStore e como migrar de SharedPreferences para o DataStore.

Como adicionar dependências

Para trabalhar com o Proto DataStore e fazer com que o Protobuf gere o código para nosso esquema, será necessário fazer várias mudanças no arquivo build.gradle:

  • Adicione o plug-in Protobuf.
  • Adicione as dependências do Protobuf e do Proto DataStore.
  • Configure o Protobuf.
plugins {
    ...
    id "com.google.protobuf" version "0.8.17"
}

dependencies {
    implementation  "androidx.datastore:datastore-core:1.0.0"
    implementation  "com.google.protobuf:protobuf-javalite:3.18.0"
    ...
}

protobuf {
    protoc {
        artifact = "com.google.protobuf:protoc:3.14.0"
    }

    // Generates the java Protobuf-lite code for the Protobufs in this project. See
    // https://github.com/google/protobuf-gradle-plugin#customizing-protobuf-compilation
    // for more information.
    generateProtoTasks {
        all().each { task ->
            task.builtins {
                java {
                    option 'lite'
                }
            }
        }
    }
}

6. Como definir e usar objetos protobuf

Buffers de protocolo são um mecanismo de serialização de dados estruturados. Você define como quer que os dados sejam estruturados uma vez e, em seguida, o compilador gera um código-fonte para gravar e ler os dados estruturados com facilidade.

Criar o arquivo proto

O esquema é definido em um arquivo proto. No nosso codelab, temos duas preferências de usuário: show_completed e sort_order. Elas são representadas como dois objetos diferentes. Um dos nossos objetivos é unificar as duas sinalizações em uma classe UserPreferences que é armazenada no DataStore. Em vez de definir essa classe em Kotlin, ela será definida com o esquema protobuf.

Confira o Guia da linguagem proto para ver informações detalhadas sobre a sintaxe. Neste codelab, vamos nos concentrar apenas nos tipos de que precisamos.

Crie um novo arquivo chamado user_prefs.proto no diretório app/src/main/proto. Se essa estrutura de pastas não for exibida, alterne para a Visualização Project. Em protobufs, cada estrutura é definida usando uma palavra-chave message e cada membro da estrutura é definido dentro da mensagem, com base no tipo e no nome, e é atribuída uma ordem baseada em 1. Vamos definir uma mensagem UserPreferences que, por enquanto, tem apenas um valor booleano chamado show_completed.

syntax = "proto3";

option java_package = "com.codelab.android.datastore";
option java_multiple_files = true;

message UserPreferences {
  // filter for showing / hiding completed tasks
  bool show_completed = 1;
}

Criar o serializador

Para informar ao DataStore como ler e gravar o tipo de dados que definimos no arquivo proto, precisamos implementar um serializador. O serializador também define o valor padrão a ser retornado se não houver dados no disco. Crie um novo arquivo com o nome UserPreferencesSerializer no pacote data:

object UserPreferencesSerializer : Serializer<UserPreferences> {
    override val defaultValue: UserPreferences = UserPreferences.getDefaultInstance()
    override suspend fun readFrom(input: InputStream): UserPreferences {
        try {
            return UserPreferences.parseFrom(input)
        } catch (exception: InvalidProtocolBufferException) {
            throw CorruptionException("Cannot read proto.", exception)
        }
    }

    override suspend fun writeTo(t: UserPreferences, output: OutputStream) = t.writeTo(output)
}

7. Como persistir os dados no Proto DataStore

Como criar o DataStore

A sinalização showCompleted é mantida na memória, no TasksViewModel, mas precisa ser armazenada no UserPreferencesRepository em uma instância DataStore.

Para criar uma instância DataStore, usamos a delegação dataStore com o Context como receptor. A delegação tem dois parâmetros obrigatórios:

  • O nome do arquivo em que o DataStore vai atuar.
  • O serializador para o tipo usado com o DataStore. No nosso caso: UserPreferencesSerializer.

Para simplificar, neste codelab, vamos fazer isso em TasksActivity:

private const val USER_PREFERENCES_NAME = "user_preferences"
private const val DATA_STORE_FILE_NAME = "user_prefs.pb"
private const val SORT_ORDER_KEY = "sort_order"

private val Context.userPreferencesStore: DataStore<UserPreferences> by dataStore(
    fileName = DATA_STORE_FILE_NAME,
    serializer = UserPreferencesSerializer
)

A delegação dataStore 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 Proto DataStore

O Proto DataStore expõe os dados armazenados em um Flow<UserPreferences>. Vamos criar um valor userPreferencesFlow: Flow<UserPreferences> público que é atribuído a dataStore.data:

val userPreferencesFlow: Flow<UserPreferences> = dataStore.data

Como gerenciar exceções ao ler dados

À medida que o DataStore lê dados de um arquivo, IOExceptions são geradas quando ocorre um erro ao ler os dados. Podemos lidar com elas usando a transformação do fluxo catch (link em inglês) e simplesmente registrando o erro:

private val TAG: String = "UserPreferencesRepo"

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) {
            Log.e(TAG, "Error reading sort order preferences.", exception)
            emit(UserPreferences.getDefaultInstance())
        } else {
            throw exception
        }
    }

Como gravar dados no Proto DataStore

Para gravar dados, o DataStore oferece uma função de suspensão DataStore.updateData(), em que o parâmetro é o estado atual de UserPreferences. Para atualizá-lo, teremos que transformar o objeto de preferências em um builder, definir o novo valor e depois criar as novas preferências.

updateData() atualiza os dados de maneira transacional em uma operação atômica de leitura-gravação-modificação. A corrotina vai ser concluída quando os dados forem armazenados no disco.

Vamos criar uma função de suspensão que nos permita atualizar a propriedade showCompleted de UserPreferences, com o nome updateShowCompleted(), que chama dataStore.updateData() e define o novo valor:

suspend fun updateShowCompleted(completed: Boolean) {
    dataStore.updateData { preferences ->
        preferences.toBuilder().setShowCompleted(completed).build()
    }
}

Nesse ponto, o app vai ser compilado, mas a funcionalidade que acabamos de criar em UserPreferencesRepository não é usada.

8. SharedPreferences para Proto DataStore

Como definir os dados a serem salvos em proto

A ordem de classificação é salva em SharedPreferences. Vamos migrá-la para o DataStore. Para fazer isso, vamos começar atualizando as UserPreferences no arquivo proto para que ele também armazene a ordem de classificação. Como a SortOrder é uma enum, é necessário a definir em UserPreference. As enums são definidas em protobufs de maneira semelhante ao Kotlin.

O valor padrão para enumerações é o primeiro listado na definição de tipo. No entanto, ao migrar de SharedPreferences, precisamos saber se o valor recebido é o valor padrão ou o definido anteriormente em SharedPreferences. Sendo assim, definimos um novo valor para nossa enumeração SortOrder: UNSPECIFIED e o listamos primeiro de modo que ele seja o valor padrão.

Nosso arquivo user_prefs.proto vai ficar assim:

syntax = "proto3";

option java_package = "com.codelab.android.datastore";
option java_multiple_files = true;

message UserPreferences {
  // filter for showing / hiding completed tasks
  bool show_completed = 1;

  // defines tasks sorting order: no order, by deadline, by priority, by deadline and priority
  enum SortOrder {
    UNSPECIFIED = 0;
    NONE = 1;
    BY_DEADLINE = 2;
    BY_PRIORITY = 3;
    BY_DEADLINE_AND_PRIORITY = 4;
  }

  // user selected tasks sorting order
  SortOrder sort_order = 2;
}

Limpe e recrie seu projeto para garantir que um novo objeto UserPreferences seja gerado com o novo campo.

Agora que a SortOrder está definida no arquivo proto, é possível remover a declaração de UserPreferencesRepository. Excluir:

enum class SortOrder {
    NONE,
    BY_DEADLINE,
    BY_PRIORITY,
    BY_DEADLINE_AND_PRIORITY
}

Confira se a importação correta da SortOrder está sendo usada em todos os lugares:

import com.codelab.android.datastore.UserPreferences.SortOrder

No TasksViewModel.filterSortTasks(), estamos fazendo ações diferentes com base no tipo SortOrder. Agora que também adicionamos a opção UNSPECIFIED, precisamos adicionar outro caso à instrução when(sortOrder). Como não queremos lidar com outras opções no momento, podemos simplesmente gerar uma UnsupportedOperationException para outros casos.

Nossa função filterSortTasks() estará assim agora:

private fun filterSortTasks(
    tasks: List<Task>,
    showCompleted: Boolean,
    sortOrder: SortOrder
): List<Task> {
    // filter the tasks
    val filteredTasks = if (showCompleted) {
        tasks
    } else {
        tasks.filter { !it.completed }
    }
    // sort the tasks
    return when (sortOrder) {
        SortOrder.UNSPECIFIED -> filteredTasks
        SortOrder.NONE -> filteredTasks
        SortOrder.BY_DEADLINE -> filteredTasks.sortedByDescending { it.deadline }
        SortOrder.BY_PRIORITY -> filteredTasks.sortedBy { it.priority }
        SortOrder.BY_DEADLINE_AND_PRIORITY -> filteredTasks.sortedWith(
            compareByDescending<Task> { it.deadline }.thenBy { it.priority }
        )
        // We shouldn't get any other values
        else -> throw UnsupportedOperationException("$sortOrder not supported")
    }
}

Como migrar de SharedPreferences

Para ajudar na migração, o DataStore define a classe SharedPreferencesMigration. O método by dataStore que cria o DataStore, usado em TasksActivity, também expõe um parâmetro produceMigrations. Neste bloco, criamos a lista das DataMigrations que precisam ser executadas para essa instância do DataStore. No nosso caso, temos apenas uma migração: a SharedPreferencesMigration.

Ao implementar uma SharedPreferencesMigration, o bloco migrate oferece dois parâmetros:

  • SharedPreferencesView que nos permite extrair dados de SharedPreferences
  • Dados atuais de UserPreferences

Será necessário retornar um objeto UserPreferences.

Ao implementar o bloco migrate, será necessário executar as seguintes etapas:

  1. Verifique o valor sortOrder em UserPreferences.
  2. Se for SortOrder.UNSPECIFIED, significa que precisamos recuperar o valor de SharedPreferences. Se não encontrar a SortOrder, poderemos usar SortOrder.NONE como padrão.
  3. Depois de recebermos a ordem de classificação, será necessário converter o objeto UserPreferences em um builder, definir a ordem de classificação e, em seguida, criar o objeto novamente chamando build(). Nenhum outro campo será afetado por essa mudança.
  4. Se o valor sortOrder nas UserPreferences não for SortOrder.UNSPECIFIED, será possível retornar os dados atuais que recebemos em migrate, porque a migração já terá sido executada.
private val Context.userPreferencesStore: DataStore<UserPreferences> by dataStore(
    fileName = DATA_STORE_FILE_NAME,
    serializer = UserPreferencesSerializer,
    produceMigrations = { context ->
        listOf(
            SharedPreferencesMigration(
                context,
                USER_PREFERENCES_NAME
            ) { sharedPrefs: SharedPreferencesView, currentData: UserPreferences ->
                // Define the mapping from SharedPreferences to UserPreferences
                if (currentData.sortOrder == SortOrder.UNSPECIFIED) {
                    currentData.toBuilder().setSortOrder(
                        SortOrder.valueOf(
                            sharedPrefs.getString(SORT_ORDER_KEY, SortOrder.NONE.name)!!
                        )
                    ).build()
                } else {
                    currentData
                }
            }
        )
    }
)

Agora que definimos a lógica de migração, precisamos fazer que o DataStore a use. Para isso, atualize o builder do DataStore e atribua ao parâmetro migrations uma nova lista que contenha uma instância de SharedPreferencesMigration:

private val dataStore: DataStore<UserPreferences> = context.createDataStore(
    fileName = "user_prefs.pb",
    serializer = UserPreferencesSerializer,
    migrations = listOf(sharedPrefsMigration)
)

Como salvar a ordem de classificação no DataStore

Para atualizar a ordem de classificação quando enableSortByDeadline() e enableSortByPriority() são chamados, precisamos fazer o seguinte:

  • Chame as respectivas funcionalidades no lambda de dataStore.updateData().
  • Como updateData() é uma função de suspensão, enableSortByDeadline() e enableSortByPriority() também precisam ser transformadas em funções de suspensão.
  • Use as UserPreferences atuais recebidas de updateData() para criar a nova ordem de classificação.
  • Atualize as UserPreferences convertendo-as para builder, depois defina a nova ordem de classificação e crie as preferências novamente.

Veja como é a implementação de enableSortByDeadline(). Você vai poder fazer as mudanças em enableSortByPriority() por conta própria.

suspend fun enableSortByDeadline(enable: Boolean) {
    // updateData handles data transactionally, ensuring that if the sort is updated at the same
    // time from another thread, we won't have conflicts
    dataStore.updateData { preferences ->
        val currentOrder = preferences.sortOrder
        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.toBuilder().setSortOrder(newSortOrder).build()
    }
}

Agora é possível remover o parâmetro de construtor context e todos os usos de SharedPreferences.

9. 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
  • private val sharedPreferences

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 proto_datastore do repositório do codelab para comparar as mudanças.

10. Resumo

Agora que você migrou para o Proto 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.