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.
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
- Android Studio Arctic Fox
- Conocer los siguientes componentes de la arquitectura: LiveData, ViewModel, Vinculación de vista y la arquitectura sugerida en la Guía de arquitectura de apps
- Conocer las corrutinas y el flujo de Kotlin
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:
- Descomprime el código y, luego, abre el proyecto en Android Studio Artic Fox.
- Ejecuta la configuración de ejecución de app en un dispositivo o emulador.
La app se ejecuta y muestra la lista de tareas:
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 elementoFlow
a fin de representar una situación más realista) - La clase
UserPreferencesRepository
, que contiene el elementoSortOrder
definido comoenum
(el orden actual se guarda en SharedPreferences comoString
, 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 elementoRecyclerView
- 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 deTasksRepository
- Un elemento
MutableStateFlow<Boolean>
que contiene la última marcashowCompleted
que solo se conserva en la memoria - Un elemento
MutableStateFlow<SortOrder>
que contiene el valorsortOrder
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 queSortOrder
, 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()
yenableSortByPriority()
. 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 | ✅ (mediante |
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 | ✅ (el trabajo se mueve a |
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:
- Verificaremos el valor de
sortOrder
enUserPreferences
. - Si es
SortOrder.UNSPECIFIED
, recuperaremos el valor de SharedPreferences. SiSortOrder
no está, usaremosSortOrder.NONE
como configuración predeterminada. - 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 abuild()
. Este cambio no afectará otros campos. - Si el valor
sortOrder
enUserPreferences
no esSortOrder.UNSPECIFIED
, podemos simplemente mostrar los datos actuales que recibimos enmigrate
, 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()
yenableSortByPriority()
también deberán serlo. - Usa el
UserPreferences
actual recibido deupdateData()
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.