Cómo trabajar con Proto DataStore

1. Introducción

¿Qué es DataStore?

DataStore es una solución nueva y mejorada de almacenamiento de datos que apunta a reemplazar SharedPreferences. Basada en corrutinas de Kotlin y Flow, proporciona dos implementaciones diferentes: Proto DataStore, que te permite almacenar objetos escritos (con el respaldo de búferes de protocolo) y Preferences DataStore, que almacena pares clave-valor. Los datos se almacenan de forma asíncrona, coherente y transaccional, por lo que resuelve algunos de los inconvenientes de SharedPreferences.

Qué aprenderás

  • Qué es DataStore y por qué deberías usarlo
  • Cómo agregar DataStore a tu proyecto
  • Las diferencias entre Preferences y Proto DataStore y las ventajas de cada uno
  • Cómo usar Proto DataStore
  • Cómo migrar de SharedPreferences a Proto DataStore

Qué compilarás

En este codelab, comenzarás con una app de ejemplo que muestra una lista de tareas que se pueden filtrar por estado completado y se pueden ordenar por prioridad y fecha límite.

fcb2ffa4e6b77f33.gif

La marca booleana para el filtro Show completed tasks se guarda en la memoria. El orden se conserva en el disco con un objeto SharedPreferences.

Como DataStore tiene dos implementaciones diferentes (Preferences DataStore y Proto DataStore), completarás las siguientes tareas de cada implementación para aprender a usar Proto DataStore:

  • Conservar el filtro de estado completo en DataStore
  • Migrar el orden de los elementos de SharedPreferences a DataStore

También te recomendamos que trabajes con el codelab de Preferences DataStore a fin de comprender mejor la diferencia entre ambos.

Requisitos

Si deseas obtener una introducción a los componentes de la arquitectura, consulta el codelab sobre Room con una View. Para obtener una introducción a los flujos, consulta el codelab sobre corrutinas avanzadas con LiveData y flujo de Kotlin.

2. Cómo prepararte

En este paso, descargarás el código de todo el codelab y, luego, ejecutarás una app de ejemplo simple.

A fin de que comiences lo antes posible, preparamos un proyecto inicial sobre el cual puedes compilar.

Si tienes Git instalado, simplemente puedes ejecutar el comando que se indica abajo. Para comprobarlo, escribe git --version en la terminal o línea de comandos y verifica que se ejecute correctamente.

 git clone https://github.com/googlecodelabs/android-datastore

El estado inicial se encuentra en la rama master. El código de la solución se encuentra en la rama proto_datastore.

Si no tienes Git, puedes hacer clic en el siguiente botón a fin de descargar todo el código de este codelab:

Descargar el código fuente

  1. Descomprime el código y, luego, abre el proyecto en Android Studio Artic Fox.
  2. Ejecuta la configuración de ejecución de app en un dispositivo o emulador.

b3c0dfdb92dfed77.png

La app se ejecuta y muestra la lista de tareas:

d3972939a2de88ba.png

3. Descripción general del proyecto

La app te permite ver una lista de tareas. Cada una tiene las siguientes propiedades: nombre, estado completado, prioridad y fecha límite.

A fin de simplificar el código con el que necesitamos trabajar, la app te permite realizar solo dos acciones:

  • Activar o desactivar la visibilidad de las tareas completadas (de forma predeterminada, estas tareas están ocultas)
  • Ordenar las tareas por prioridad, por fecha límite o por fecha límite y prioridad

La app sigue la arquitectura recomendada en la Guía de arquitectura de apps. En cada paquete, encontrarás lo siguiente:

data

  • La clase modelo Task
  • La clase TasksRepository, responsable de proporcionar las tareas (para simplificar, muestra datos codificados y los expone mediante un elemento Flow a fin de representar una situación más realista)
  • La clase UserPreferencesRepository, que contiene el elemento SortOrder definido como enum (el orden actual se guarda en SharedPreferences como String, según el nombre del valor enum, y expone métodos síncronos para guardar y obtener el orden)

ui

  • Clases relacionadas con la visualización de un elemento Activity con un elemento RecyclerView
  • La clase TasksViewModel, encargada de la lógica de la IU

TasksViewModel contiene todos los elementos necesarios para compilar los datos que se deben mostrar en la IU: la lista de tareas y las marcas showCompleted y sortOrder, dentro de un objeto TasksUiModel. Cada vez que cambia uno de estos valores, debemos reconstruir un nuevo elemento TasksUiModel. Para ello, combinamos 3 elementos:

  • Un elemento Flow<List<Task>> que se recupera de TasksRepository
  • Un elemento MutableStateFlow<Boolean> que contiene la última marca showCompleted que solo se conserva en la memoria
  • Un elemento MutableStateFlow<SortOrder> que contiene el valor sortOrder más reciente

Para asegurarnos de estar actualizando la IU de forma correcta, solo cuando se inicia el elemento Activity, exponemos un LiveData<TasksUiModel>.

Tenemos algunos problemas con nuestro código:

  • Bloqueamos el subproceso de IU en la E/S del disco cuando se inicializa UserPreferencesRepository.sortOrder. Esto puede ocasionar bloqueos de IU.
  • La marca showCompleted solo se conserva en la memoria, por lo que se restablecerá cada vez que el usuario abra la app. Al igual que SortOrder, debería conservarse incluso después de cerrar la app.
  • Actualmente, usamos SharedPreferences para conservar datos, pero conservamos un elemento MutableStateFlow en la memoria, que se modifica manualmente a fin de recibir notificaciones sobre los cambios. El elemento se rompe con facilidad si el valor se modifica en otro lugar de la aplicación.
  • En UserPreferencesRepository, exponemos dos métodos para actualizar el orden: enableSortByDeadline() y enableSortByPriority(). Ambos métodos dependen del valor actual del orden de clasificación, pero, si se llama a uno antes de que el otro termine, tendremos un valor final incorrecto. Además, estos métodos pueden provocar bloqueos de IU e incumplimientos del modo estricto a medida que se los llama en el subproceso de IU.

Aunque las marcas showCompleted y sortOrder son preferencias del usuario, en la actualidad, se representan como dos objetos diferentes. Por lo tanto, uno de nuestros objetivos será unificar estas dos marcas en una clase UserPreferences.

Veamos cómo usar DataStore para resolver estos problemas.

4. DataStore: conceptos básicos

Es posible que necesites almacenar conjuntos de datos pequeños o simples con frecuencia. Para ello, en el pasado, quizás habrías usado SharedPreferences, aunque esta API también tiene algunas desventajas. La biblioteca de Jetpack DataStore tiene como objetivo abordar esos problemas con una API simple, segura y asíncrona para almacenar datos. Brinda 2 implementaciones diferentes:

  • Preferences DataStore
  • Proto DataStore

Función

SharedPreferences

Preferences DataStore

Proto DataStore

API asíncrona

✅ (solo para leer los valores modificados, mediante un objeto de escucha)

✅ (mediante Flow y RxJava 2 y 3 Flowable)

✅ (mediante Flow y RxJava 2 y 3 Flowable)

API síncrona

✅ (pero no es seguro llamarla en el subproceso de IU)

Es seguro llamarla en el subproceso de IU

❌(1)

✅ (el trabajo se mueve a Dispatchers.IO debajo de la superficie)

✅ (el trabajo se mueve a Dispatchers.IO debajo de la superficie)

Puede indicar errores

Seguro contra excepciones de tiempo de ejecución

❌(2)

Tiene una API transaccional con garantías de coherencia sólida

Controla la migración de datos

Tipo de seguridad

✅ con búferes de protocolo

(1) SharedPreferences tiene una API síncrona que puede parecer segura para realizar llamadas en el subproceso de IU, pero que en realidad realiza operaciones de E/S en el disco. Además, apply() bloquea el subproceso de IU en fsync(). Las llamadas de fsync() pendientes se activan cada vez que se inicia o se detiene un servicio, y cada vez que se inicia o se detiene una actividad en tu aplicación. El subproceso de IU está bloqueado en las llamadas pendientes fsync() programadas por apply(), que suelen convertirse en una fuente de ANR.

(2) SharedPreferences genera errores de análisis como excepciones de tiempo de ejecución.

Preferences DataStore vs. Proto DataStore

Si bien Preferences y Proto DataStore permiten el almacenamiento de datos, lo hacen de diferente manera:

  • Preferences DataStore, al igual que SharedPreferences, accede a los datos según las claves, sin definir un esquema por adelantado.
  • Proto DataStore define el esquema con búferes de protocolo. El uso de protobufs permite conservar los datos de tipado fuerte. Son más rápidos, más pequeños, más simples y menos ambiguos que XML y otros formatos de datos similares. Si bien Proto DataStore requiere que aprendas un mecanismo de serialización nuevo, creemos que la ventaja del tipado fuerte que proporciona Proto DataStore vale la pena.

Room vs. DataStore

Si necesitas actualizaciones parciales, integridad referencial o conjuntos de datos grandes o complejos, debes considerar usar Room en lugar de DataStore. DataStore es ideal para conjuntos de datos pequeños y simples, y no admite actualizaciones parciales ni integridad referencial.

5. Descripción general de Proto DataStore

Una de las desventajas de SharedPreferences y Preferences DataStore es que no hay manera de definir un esquema ni garantizar que se acceda a las claves con el tipo correcto. Proto DataStore soluciona este problema por medio del uso de búferes de protocolo para definir el esquema. Con el uso de los protocolos, DataStore sabrá qué tipos están almacenados y los proporcionará, lo que elimina la necesidad de usar claves.

Veamos cómo agregar Proto DataStore y Protobufs al proyecto, qué son los búferes de protocolo, cómo usarlos con Proto DataStore y cómo migrar SharedPreferences a DataStore.

Cómo agregar dependencias

A fin de trabajar con Proto DataStore y hacer que Protobuf genere código para nuestro esquema, tendremos que realizar varios cambios en el archivo build.gradle:

  • Agrega el complemento de Protobuf.
  • Agrega las dependencias de Protobuf y Proto DataStore.
  • Configura 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. Cómo definir y usar objetos protobufs

Los búferes de protocolo son un mecanismo para serializar datos estructurados. Debes definir la forma en que deseas que se estructuren tus datos una vez y, luego, el compilador generará código fuente a fin de escribir y leer los datos estructurados con facilidad.

Cómo crear el archivo .proto

Define el esquema en un archivo .proto. En nuestro codelab tenemos 2 preferencias de usuario: show_completed y sort_order. En la actualidad, estas se representan como dos objetos diferentes. Uno de nuestros objetivos es unificar estas dos marcas en una clase UserPreferences que se almacene en DataStore. En lugar de definir esta clase en Kotlin, la definiremos en el esquema de protobuf.

Consulta la guía de lenguaje proto a fin de obtener información detallada sobre la sintaxis. En este codelab, solo nos enfocaremos en los tipos que necesitamos.

Crea un archivo nuevo llamado user_prefs.proto en el directorio app/src/main/proto. Si no puedes ver esta estructura de carpetas, cambia a Project view. En los protobufs, cada estructura se define por medio de una palabra clave message, y cada miembro de la estructura se define dentro del mensaje, según el tipo y el nombre, y se le asigna un orden basado en 1. Definamos un mensaje UserPreferences que, por ahora, solo tenga un valor booleano llamado 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;
}

Crea el serializador

Para indicarle a DataStore cómo leer y escribir el tipo de datos que definimos en el archivo .proto, debemos implementar un Serializador. Este también define el valor predeterminado que se mostrará si no hay datos en el disco. Crea un archivo nuevo llamado UserPreferencesSerializer en el paquete 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. Datos persistentes en Proto DataStore

Cómo crear DataStore

La marca showCompleted se conserva en la memoria, en TasksViewModel, pero debería almacenarse en UserPreferencesRepository, en una instancia de DataStore.

Para crear una instancia de DataStore, usamos el delegado dataStore con Context como receptor. El delegado tiene dos parámetros obligatorios:

  • El nombre del archivo en el que actuará DataStore
  • El serializador para el tipo usado con DataStore (en nuestro caso: UserPreferencesSerializer)

Para mayor simplicidad, en este codelab, lo haremos en 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
)

El delegado dataStore garantiza que tengamos una sola instancia de DataStore con ese nombre en nuestra aplicación. Actualmente, UserPreferencesRepository se implementa como un singleton, porque contiene sortOrderFlow y evita que se vincule al ciclo de vida de TasksActivity. Debido a que UserPreferenceRepository trabajará con los datos de DataStore y no creará ni conservará objetos nuevos, podemos quitar la implementación del singleton:

  • Quita el companion object.
  • Haz que constructor sea público.

El objeto UserPreferencesRepository debería obtener una instancia de DataStore como parámetro constructor. Por ahora, podemos dejar Context como parámetro, ya que SharedPreferences lo necesita, pero lo quitaremos más tarde.

class UserPreferencesRepository(
    private val userPreferencesStore: DataStore<UserPreferences>,
    context: Context
) { ... }

Actualicemos la construcción de UserPreferencesRepository en TasksActivity y pasemos dataStore:

viewModel = ViewModelProvider(
    this,
    TasksViewModelFactory(
        TasksRepository,
        UserPreferencesRepository(dataStore, this)
    )
).get(TasksViewModel::class.java)

Cómo leer los datos de Proto DataStore

Proto DataStore expone los datos almacenados en un Flow<UserPreferences>. Creemos un valor público userPreferencesFlow: Flow<UserPreferences> que se asigne a dataStore.data:

val userPreferencesFlow: Flow<UserPreferences> = dataStore.data

Cómo administrar excepciones mientras se leen los datos

A medida que DataStore lee datos de un archivo, se generan IOException cuando se produce un error durante la lectura de esos datos. Para controlarlos, podemos usar la transformación de Flow catch y simplemente registrar el error:

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

Cómo escribir datos en Proto DataStore

Para escribir datos, DataStore ofrece una función de suspensión DataStore.updateData(), en la que obtenemos como parámetro el estado actual de UserPreferences. Para actualizarla, debemos transformar el objeto de preferencias al compilador, establecer el valor nuevo y luego compilar las preferencias nuevas.

updateData() actualiza los datos de forma transaccional en una operación atómica de lectura, escritura y modificación. La corrutina se completa una vez que los datos se mantienen en el disco.

Creemos una función de suspensión que nos permita actualizar la propiedad showCompleted de UserPreferences, llamada updateShowCompleted(), que llame a dataStore.updateData() y establezca el valor nuevo:

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

En este punto, la app debería compilarse, pero la funcionalidad que acabamos de crear en UserPreferencesRepository no se usa.

8. SharedPreferences a Proto DataStore

Cómo definir los datos que se guardarán en proto

El orden se guarda en SharedPreferences. Trasladémoslo a DataStore. Para comenzar, actualicemos UserPreferences en el archivo .proto a fin de almacenar el orden de clasificación: Como SortOrder es una enum, tendremos que definirlo en nuestro UserPreference. Las enums se definen en protobufs de manera similar a Kotlin.

En el caso de las enumeraciones, el valor predeterminado es el primer valor que aparece en la definición de tipo de la enum. Sin embargo, cuando migremos desde SharedPreferences, deberemos saber si el valor que obtuvimos es el predeterminado o el que se configuró anteriormente en SharedPreferences. A fin de resolver esto, definiremos un valor nuevo para nuestra enumeración de SortOrder, UNSPECIFIED, y lo colocaremos primero en la lista a fin de que sea el valor predeterminado.

Nuestro archivo user_prefs.proto debería verse de la siguiente manera:

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;
}

Limpia y vuelve a compilar tu proyecto para asegurarte de que se genere un objeto UserPreferences nuevo que contenga el campo nuevo.

Ahora que SortOrder está definido en el archivo .proto, podemos quitar la declaración de UserPreferencesRepository. Borra lo siguiente:

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

Asegúrate de que se use la importación SortOrder correcta en todas partes:

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

En el TasksViewModel.filterSortTasks(), realizamos diferentes acciones según el tipo de SortOrder. Ahora que agregamos la opción UNSPECIFIED, deberemos agregar otro caso para la declaración when(sortOrder). Como no queremos controlar otras opciones excepto aquellas en las que estamos, podemos arrojar una UnsupportedOperationException en los otros casos.

Nuestra función filterSortTasks() ahora se ve de la siguiente manera:

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")
    }
}

Migración desde SharedPreferences

Para ayudar con la migración, DataStore define la clase SharedPreferencesMigration. El método by dataStore que crea DataStore (que se usa en TasksActivity), también expone un parámetro produceMigrations. En este bloque, creamos la lista de DataMigration que debería ejecutarse para esta instancia de DataStore. En nuestro caso, solo tenemos una migración: SharedPreferencesMigration.

Cuando implementamos un objeto SharedPreferencesMigration, el bloque migrate nos proporciona dos parámetros:

  • SharedPreferencesView, que nos permite recuperar los datos de SharedPreferences
  • Datos actuales de UserPreferences

Debemos mostrar un objeto UserPreferences.

Cuando implementemos el bloque migrate, tendremos que seguir estos pasos:

  1. Verificaremos el valor de sortOrder en UserPreferences.
  2. Si es SortOrder.UNSPECIFIED, recuperaremos el valor de SharedPreferences. Si SortOrder no está, usaremos SortOrder.NONE como configuración predeterminada.
  3. Una vez que obtengamos el orden de clasificación, tendremos que convertir el objeto UserPreferences al compilador, establecer el orden y volver a compilar el objeto llamando a build(). Este cambio no afectará otros campos.
  4. Si el valor sortOrder en UserPreferences no es SortOrder.UNSPECIFIED, podemos simplemente mostrar los datos actuales que recibimos en migrate, ya que la migración ya debería haberse ejecutado de forma correcta.
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
                }
            }
        )
    }
)

Ahora que definimos la lógica de migración, necesitamos indicarle a DataStore que debe usarla. Para ello, actualiza el compilador de DataStore y asigna al parámetro migrations una lista nueva que contenga una instancia de nuestro SharedPreferencesMigration:

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

Cómo guardar el orden en DataStore

Para actualizar el orden de clasificación cuando se llama a enableSortByDeadline() y enableSortByPriority(), debemos hacer lo siguiente:

  • Llama a sus respectivas funcionalidades en la lambda de dataStore.updateData().
  • Dado que updateData() es una función de suspensión, enableSortByDeadline() y enableSortByPriority() también deberán serlo.
  • Usa el UserPreferences actual recibido de updateData() para construir el nuevo orden de clasificación.
  • Actualiza el UserPreferences convirtiéndolo al compilador, configura el orden de clasificación nuevo y vuelve a compilar las preferencias.

Así se ve la implementación de enableSortByDeadline(). Te dejaremos que realices los cambios en enableSortByPriority() por tu cuenta.

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()
    }
}

Ahora, puedes quitar el parámetro de constructor context y todos los usos de SharedPreferences.

9. Cómo actualizar TasksViewModel para usar UserPreferencesRepository

Ahora que UserPreferencesRepository almacena las marcas show_completed y sort_order en DataStore y expone una Flow<UserPreferences>, actualicemos TasksViewModel para usarlas.

Quita showCompletedFlow y sortOrderFlow y, en su lugar, crea un valor llamado userPreferencesFlow que se inicialice con userPreferencesRepository.userPreferencesFlow:

private val userPreferencesFlow = userPreferencesRepository.userPreferencesFlow

En la creación de tasksUiModelFlow, reemplaza showCompletedFlow y sortOrderFlow con userPreferencesFlow. Reemplaza los parámetros según corresponda.

Cuando llamas a filterSortTasks, pasa showCompleted y sortOrder de las userPreferences. Tu código debería verse de la siguiente manera:

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

Se debe actualizar la función showCompletedTasks() para llamar a userPreferencesRepository.updateShowCompleted(). Como esta es una función de suspensión, crea una corrutina nueva en viewModelScope:

fun showCompletedTasks(show: Boolean) {
    viewModelScope.launch {
        userPreferencesRepository.updateShowCompleted(show)
    }
}

Las funciones de userPreferencesRepository, enableSortByDeadline() y enableSortByPriority() ahora son funciones de suspensión, de modo que también deberían llamarse en una corrutina nueva, iniciada en viewModelScope:

fun enableSortByDeadline(enable: Boolean) {
    viewModelScope.launch {
       userPreferencesRepository.enableSortByDeadline(enable)
    }
}

fun enableSortByPriority(enable: Boolean) {
    viewModelScope.launch {
        userPreferencesRepository.enableSortByPriority(enable)
    }
}

Limpieza de UserPreferencesRepository

Quitemos los campos y métodos que ya no son necesarios. Deberías poder borrar los siguientes elementos:

  • _sortOrderFlow
  • sortOrderFlow
  • updateSortOrder()
  • private val sortOrder: SortOrder
  • private val sharedPreferences

Ahora, nuestra app debería compilarse correctamente. Ejecutémosla a fin de ver si las marcas show_completed y sort_order se guardaron correctamente.

Revisa la rama proto_datastore del repositorio de codelab para comparar tus cambios.

10. Conclusión

Ahora que migraste a Proto DataStore, veamos qué aprendimos:

  • SharedPreferences trae una serie de desventajas: una API sincrónica que puede parecer segura para realizar llamadas en el subproceso de IU, ningún mecanismo para señalar errores y falta de API de transacciones, entre otras.
  • DataStore es un reemplazo para SharedPreferences que aborda la mayoría de las deficiencias de la API.
  • DataStore tiene una API completamente asíncrona que usa corrutinas de Kotlin y Flow y que administra la migración de datos, garantiza su coherencia y resuelve su corrupción.