Preferences DataStore

1. Antes de comenzar

En codelabs anteriores, aprendiste a guardar datos en una base de datos SQLite con Room, una capa de abstracción de base de datos. En este codelab, se presenta Jetpack DataStore. DataStore, que se basa en corrutinas y flujo de Kotlin, proporciona dos implementaciones diferentes: Proto DataStore, que almacena objetos escritos, y Preferences DataStore, que almacena pares clave-valor.

En este codelab práctico, aprenderás a usar Preferences DataStore. Proto DataStore no se incluye en este codelab.

Requisitos previos

  • Debes conocer los componentes de la arquitectura de Android ViewModel, LiveData y Flow, y saber cómo usar ViewModelProvider.Factory para crear una instancia de ViewModel.
  • Debes conocer los conceptos básicos de simultaneidad.
  • Debes saber cómo usar corrutinas para tareas de larga duración.

Qué aprenderás

  • Qué es DataStore, cuándo se debe usar y por qué se recomienda hacerlo
  • Cómo agregar Preferences DataStore a tu app

Qué necesitarás

  • El código de inicio para la app de Words (es igual al código de solución de esta app presentado en un codelab anterior)
  • Una computadora que tenga Android Studio instalado

Cómo descargar el código de partida para este codelab

En este codelab, extenderás las funciones de la app de Words de códigos de solución anteriores. El código de partida puede contener código que ya conoces de codelabs anteriores.

A fin de obtener el código de este codelab desde GitHub y abrirlo en Android Studio, haz lo siguiente:

  1. Inicia Android Studio.
  2. En la ventana Welcome to Android Studio, haz clic en Get from VCS.

61c42d01719e5b6d.png

  1. En el diálogo Get from Version Control, asegúrate de que esté seleccionado Git para Version control.

9284cfbe17219bbb.png

  1. Pega la URL del código proporcionado en el cuadro URL.
  2. Si lo prefieres, cambia el Directorio a uno diferente al predeterminado sugerido.

5ddca7dd0d914255.png

  1. Haz clic en Clonar. Android Studio comenzará a recuperar tu código.
  2. Espera a que se abra Android Studio.
  3. Selecciona el módulo correcto para tu activador de codelab, tu app o tu código de solución.

2919fe3e0c79d762.png

  1. Haz clic en el botón Run 8de56cba7583251f.png para compilar y ejecutar el código.

2. Descripción general de la app de inicio

La app de Words consta de dos pantallas: la primera muestra letras que el usuario puede seleccionar y la segunda muestra una lista de palabras que comienzan con las letras seleccionadas.

Esta app tiene una opción de menú para que el usuario active y desactive los diseños de lista y cuadrícula para las letras.

  1. Descarga el código de partida, ábrelo en Android Studio y ejecuta la app. Las letras se muestran en un diseño lineal.
  2. En la esquina superior derecha, presiona la opción de menú. El diseño cambia a uno de cuadrícula.
  3. Sal de la app y vuelve a iniciarla. Para ello, usa las opciones Stop 'app' f782441b99bdd0a4.png y Run 'app' d203bd07cbce5954.png en Android Studio. Ten en cuenta que cuando se reinicie la app, las letras se mostrarán en un diseño lineal y no en una cuadrícula.

Observa que no se retendrá la selección del usuario. En este codelab, se muestra cómo solucionar ese problema.

Qué compilarás

  • En este codelab, aprenderás a usar Preferences DataStore para conservar los parámetros de configuración del diseño en DataStore.

3. Introducción a Preferences DataStore

Preferences DataStore es ideal para conjuntos de datos pequeños y simples, como el almacenamiento de datos de inicio de sesión, la configuración del modo oscuro, el tamaño de la fuente, entre otros. DataStore no es adecuado para conjuntos de datos complejos, como una lista de inventario de tiendas de alimentos en línea o una base de datos de alumnos. Si necesitas almacenar conjuntos de datos grandes o complejos, te recomendamos usar Room en lugar de DataStore.

Con la biblioteca de Jetpack DataStore, puedes crear una API simple, segura y asíncrona para almacenar datos. Esta biblioteca ofrece dos implementaciones diferentes: Preferences DataStore y Proto DataStore. Si bien Preferences y Proto DataStore permiten el almacenamiento de datos, lo hacen de diferente manera:

  • Preferences DataStore accede a los datos y los almacena en función de claves, sin definir un esquema (modelo de base de datos) por adelantado.
  • Proto DataStore define el esquema mediante búferes de protocolo. El uso de estos búferes, llamados Protobufs, te 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.

Diferencias entre Room y Datastore: cuándo usarlos

Si tu aplicación necesita almacenar datos grandes o complejos en un formato estructurado, como SQL, considera usar Room. Sin embargo, si solo necesitas almacenar cantidades simples o pequeñas de datos que se pueden guardar en pares clave-valor, DataStore es ideal.

Diferencias entre Proto y Preferences DataStore: cuándo usarlos

Proto DataStore es seguro y eficiente, pero requiere configuración. Si los datos de tu app son lo suficientemente sencillos como para guardarlos en pares clave-valor, Preferences DataStore es una mejor opción, ya que es mucho más fácil de configurar.

Cómo agregar Preferences DataStore como dependencia

El primer paso para integrar DataStore con tu app es agregarlo como dependencia.

  1. En build.gradle(Module: Words.app), agrega la siguiente dependencia:
implementation "androidx.datastore:datastore-preferences:1.0.0"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1"

4. Crea un Preferences DataStore

  1. Agrega un paquete llamado data y crea una clase de Kotlin llamada SettingsDataStore dentro de este.
  2. Agrega un parámetro de constructor a la clase SettingsDataStore del tipo Context.
class SettingsDataStore(context: Context) {}
  1. Fuera de la clase SettingsDataStore, declara un elemento private const val llamado LAYOUT_PREFERENCES_NAME y asígnale el valor de cadena layout_preferences. Este es el nombre de Preferences Datastore del que crearás una instancia en el siguiente paso.
private const val LAYOUT_PREFERENCES_NAME = "layout_preferences"
  1. Cuando aún estés fuera de la clase, crea una instancia de DataStore con el delegado preferencesDataStore. Dado que estás usando Preferences Datastore, deberás pasar Preferences como un tipo de almacén de datos. Además, establece el almacén de datos name en LAYOUT_PREFERENCES_NAME.

El código completo es el siguiente:

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. Implementa la clase SettingsDataStore

Como se indicó, Preferences DataStore almacena los datos en pares clave-valor. En este paso, deberás definir las claves necesarias para almacenar los parámetros de configuración del diseño y definir las funciones de escritura y lectura en Preferences DataStore.

Funciones de tipo de clave

Preferences DataStore no usa un esquema predefinido como Room, sino funciones de tipo de clave correspondientes para definir una clave para cada valor que almacenes en la instancia de DataStore<Preferences>. Por ejemplo, si deseas definir una clave para un valor int, usa intPreferencesKey() y, para un valor string, usa stringPreferencesKey(). En general, estos nombres de funciones tienen el prefijo del tipo de datos que quieres almacenar en la clave.

Implementa lo siguiente en la clase data\SettingsDataStore:

  1. Con el fin de implementar la clase SettingsDataStore, el primer paso es crear una clave que almacene un valor booleano que especifique si el parámetro de configuración del usuario es un diseño lineal. Crea una propiedad de clase private llamada IS_LINEAR_LAYOUT_MANAGER, inicialízala con booleanPreferencesKey() y pasa el nombre de clave is_linear_layout_manager como el parámetro de la función.
private val IS_LINEAR_LAYOUT_MANAGER = booleanPreferencesKey("is_linear_layout_manager")

Escribe en Preferences DataStore

Ahora es el momento de usar la clave y almacenar los parámetros de configuración del diseño booleano en DataStore. Preferences DataStore proporciona una función de suspensión edit() que actualiza los datos de forma transaccional en DataStore. El parámetro de transformación de la función acepta un bloque de código en el que puedes actualizar los valores según sea necesario. Todo el código que esté en el bloque de transformación se tratará como una sola transacción. De forma interna, el trabajo de la transacción se mueve a Dispacter.IO, así que no olvides establecer tu función suspend cuando llames a la función edit().

  1. Crea una función suspend llamada saveLayoutToPreferencesStore() que tome dos parámetros: el valor booleano de configuración del diseño y el Context.
suspend fun saveLayoutToPreferencesStore(isLinearLayoutManager: Boolean, context: Context) {

}
  1. Implementa la función anterior, llama a dataStore.edit() y pasa un bloque de código para almacenar el valor nuevo.
suspend fun saveLayoutToPreferencesStore(isLinearLayoutManager: Boolean, context: Context) {
   context.dataStore.edit { preferences ->
       preferences[IS_LINEAR_LAYOUT_MANAGER] = isLinearLayoutManager
   }
}

Lee desde Preferences DataStore

Preferences DataStore expone los datos almacenados en un Flow<Preferences> que se emiten cada vez que se modifique una preferencia. No te recomendamos que expongas todo el objeto Preferences, sino solo el valor Boolean. Para ello, asignamos el Flow<Preferences> y obtenemos el valor Boolean que te interesa.

  1. Expón un preferenceFlow: Flow<UserPreferences>, construido sobre dataStore.data: Flow<Preferences>, y asígnalo para recuperar la preferencia Boolean. Dado que Datastore está vacío en la primera ejecución, muestra true de forma predeterminada.
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. Agrega las siguientes importaciones si no se importan automáticamente:
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map

Manejo de excepciones

A medida que DataStore lee y escribe datos de archivos, es posible que IOExceptions ocurran cuando se acceda a los datos. Puedes manejarlas usando el operador catch() para detectar excepciones.

  1. SharedPreference DataStore arroja una IOException cuando se encuentra un error durante la lectura de los datos. En la declaración de preferenceFlow, antes de map(), usa el operador catch() a fin de detectar la IOException y emitir emptyPreferences(). Para evitar complicaciones, ya que no esperamos ningún otro tipo de excepción en este caso, si se arroja un tipo distinto de excepción, vuelve a arrojarla.
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
   }

Tu clase data\SettingsDataStore está lista para usarse.

6. Usa la clase SettingsDataStore

En esta próxima tarea, usarás SettingsDataStore en tu clase LetterListFragment. Adjuntarás un observador a los parámetros de configuración del diseño y actualizarás la IU según corresponda.

Implementa los siguientes pasos en LetterListFragment:

  1. Declara una variable de clase private llamada SettingsDataStore de tipo SettingsDataStore. Haz que esta variable sea lateinit, ya que la inicializarás más tarde.
private lateinit var SettingsDataStore: SettingsDataStore
  1. Al final de la función onViewCreated(), inicializa la variable nueva y pasa el requireContext() al constructor SettingsDataStore.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   ...
   // Initialize SettingsDataStore
   SettingsDataStore = SettingsDataStore(requireContext())
}

Lee y observa los datos

  1. En LetterListFragment, dentro del método onViewCreated(), debajo de la inicialización de SettingsDataStore, convierte preferenceFlow en Livedata usando asLiveData(). Adjunta un observador y pasa viewLifecycleOwner como el propietario.
SettingsDataStore.preferenceFlow.asLiveData().observe(viewLifecycleOwner, { })
  1. Dentro del observador, asigna los nuevos parámetros de configuración del diseño a la variable isLinearLayoutManager. Realiza una llamada a la función chooseLayout() para actualizar el diseño de RecyclerView.
SettingsDataStore.preferenceFlow.asLiveData().observe(viewLifecycleOwner, { value ->
           isLinearLayoutManager = value
           chooseLayout()
   })

La función onViewCreated() completa debería ser similar a la siguiente:

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

Escribe los parámetros de configuración del diseño en DataStore

El último paso consiste en escribir los parámetros de configuración del diseño en Preferences DataStore cuando el usuario presione la opción del menú. La escritura de datos en el almacén de datos de preferencias debe realizarse de forma asíncrona dentro de una corrutina. Para hacerlo dentro de un fragmento, usa el CoroutineScope llamado LifecycleScope.

LifecycleScope

Los componentes optimizados para ciclos de vida, como los fragmentos, proporcionan compatibilidad de primer nivel con las corrutinas para alcances lógicos de tu app, junto con una capa de interoperabilidad con LiveData. Se define un LifecycleScope para cada objeto Lifecycle. Se cancelan todas las corrutinas iniciadas en este alcance cuando se destruye el propietario del Lifecycle.

  1. En LetterListFragment, dentro de la función onOptionsItemSelected(), al final del caso R.id.action_switch_layout, inicia la corrutina mediante lifecycleScope. Dentro del bloque launch, realiza una llamada al saveLayoutToPreferencesStore() y pasa el isLinearLayoutManager y el 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. Ejecuta la app. Haz clic en la opción del menú para cambiar el diseño de la app.

  1. Ahora prueba la persistencia de Preferences DataStore. Cambia el diseño de la app a un diseño de cuadrícula. Sal de la app y vuelve a iniciarla (para ello, usa las opciones Stop 'app' f782441b99bdd0a4.png y Run 'app' d203bd07cbce5954.png en Android Studio).

cd2c31f27dfb5157.png

Cuando se reinicie la app, las letras se mostrarán en un diseño de cuadrícula y no en el diseño lineal. Tu app está guardando correctamente los parámetros de configuración del diseño que seleccionó el usuario.

Ten en cuenta que, aunque ahora las letras se muestran en un diseño de cuadrícula, el ícono de menú no se actualiza correctamente. A continuación, veremos cómo solucionar este problema.

7. Corrige el error del ícono de menú

El motivo del error en el ícono de menú es que, en onViewCreated(), se actualiza el diseño de RecyclerView según los parámetros de configuración del diseño, pero no el ícono de menú. Para solucionar este problema, vuelve a dibujar el menú y actualiza el diseño de RecyclerView.

Cómo volver a dibujar el menú de opciones

Una vez que se crea un menú, no se vuelve a dibujar cada marco, ya que sería redundante volver a dibujar el mismo menú en cada marco. La función invalidateOptionsMenu() le indica a Android que vuelva a dibujar el menú de opciones.

Puedes llamar a esta función cuando cambias algo en el menú de opciones, como agregar un elemento de menú, borrar un elemento o cambiar el texto o el ícono del menú. En este caso, se cambió el ícono de menú. Si llamas a este método, se declarará que cambió el menú de opciones, por lo que deberás volver a crearlo. Se llamará al método onCreateOptionsMenu(android.view.Menu) la próxima vez que se deba mostrar.

  1. En LetterListFragment, dentro de onViewCreated(), al final del observador de preferenceFlow, debajo de la llamada a chooseLayout(). Vuelve a dibujar el menú llamando a invalidateOptionsMenu() en la activity.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   ...
   SettingsDataStore.preferenceFlow.asLiveData().observe(viewLifecycleOwner, { value ->
           ...
           // Redraw the menu
           activity?.invalidateOptionsMenu()
   })
}
  1. Vuelve a ejecutar la app y cambia el diseño.
  2. Sal de la app y vuelve a iniciarla. Observa que el ícono de menú ahora se actualizó correctamente.

1c8cf63c8d175aad.png

¡Felicitaciones! Agregaste Preferences DataStore a tu app de forma correcta para guardar la selección de los usuarios.

8. Código de solución

El código de la solución para este codelab se encuentra en el proyecto y módulo que se muestran a continuación.

9. Resumen

  • DataStore tiene una API completamente asíncrona que usa corrutinas y flujo de Kotlin, y que garantiza la coherencia de los datos.
  • Jetpack Datastore es una solución de almacenamiento de datos que te permite almacenar objetos escritos con búferes de protocolo o pares clave-valor.
  • DataStore ofrece dos implementaciones diferentes: Preferences DataStore y Proto DataStore.
  • Preferences DataStore no usa un esquema predefinido.
  • Preferences DataStore utiliza la función de tipo de clave correspondiente a fin de definir una clave para cada valor que necesites almacenar en la instancia DataStore<Preferences>. Por ejemplo, si deseas definir una clave para un valor int, usa intPreferencesKey().
  • Preferences DataStore proporciona una función edit()que actualiza los datos de forma transaccional en un DataStore.

10. Más información

Blog

Opta por almacenar datos con Jetpack Datastore