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
yFlow
, y saber cómo usarViewModelProvider.Factory
para crear una instancia deViewModel
. - 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
Requisitos
- 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 inicio 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 inicio 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:
- Inicia Android Studio.
- En la ventana Welcome to Android Studio, haz clic en Check out project from version control.
- Elige Git.
- En el cuadro de diálogo Clone Repository, pega la URL del código proporcionado en el cuadro URL.
- Haz clic en el botón Test, espera y asegúrate de que haya un cuadro emergente de color verde que diga Connection successful.
- De manera opcional, cambia el Directory a uno diferente al predeterminado sugerido.
- Haz clic en Clone. Android Studio comenzará a recuperar tu código.
- En la ventana emergente Checkout from Version Control, haz clic en Yes.
- Espera a que se abra Android Studio.
- Selecciona el módulo correcto para tu inicio del codelab o código de la solución.
- Haz clic en el botón Run 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.
- Descarga el código de inicio, ábrelo en Android Studio y ejecuta la app. Las letras se muestran en un diseño lineal.
- En la esquina superior derecha, presiona la opción de menú. El diseño cambia a uno de cuadrícula.
- Sal de la app y vuelve a iniciarla. Para ello, usa las opciones Stop 'app' y Run 'app' 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 retenga 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 proporciona 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.
- 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
- Agrega un paquete llamado
data
y crea una clase de Kotlin llamadaSettingsDataStore
dentro de este. - Agrega un parámetro de constructor a la clase
SettingsDataStore
del tipoContext
.
class SettingsDataStore(context: Context) {}
- Fuera de la clase
SettingsDataStore
, declara un elementoprivate const val
llamadoLAYOUT_PREFERENCES_NAME,
y asígnale el valor de stringlayout_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"
- Cuando aún estés fuera de la clase, crea una instancia de
DataStore
con el delegadopreferencesDataStore
. Dado que estás usando Preferences Datastore, deberás pasarPreferences
como un tipo de almacén de datos. Además, establece el almacén de datosname
enLAYOUT_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 a fin de 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
:
- 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 claseprivate
llamadaIS_LINEAR_LAYOUT_MANAGER,
, inicialízala conbooleanPreferencesKey()
y pasa el nombre de claveis_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()
.
- Crea una función
suspend
llamadasaveLayoutToPreferencesStore()
que tome dos parámetros: el valor booleano de configuración del diseño y elContext
.
suspend fun saveLayoutToPreferencesStore(isLinearLayoutManager: Boolean, context: Context) {
}
- 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.
- Expón un
preferenceFlow: Flow<UserPreferences>
, construido sobredataStore.data: Flow<Preferences>
, y asígnalo para recuperar la preferenciaBoolean
. Dado que Datastore está vacío en la primera ejecución, muestratrue
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
}
- 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.
- SharedPreference DataStore arroja una
IOException
cuando se encuentra un error durante la lectura de los datos. En la declaración depreferenceFlow
, antes demap()
, usa el operadorcatch()
a fin de detectar laIOException
y emitiremptyPreferences()
. 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
.
- Declara una variable de clase
private
llamadaSettingsDataStore
de tipoSettingsDataStore
. Haz que esta variable sealateinit
, ya que la inicializarás más tarde.
private lateinit var SettingsDataStore: SettingsDataStore
- Al final de la función
onViewCreated()
, inicializa la variable nueva y pasa elrequireContext()
al constructorSettingsDataStore
.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
...
// Initialize SettingsDataStore
SettingsDataStore = SettingsDataStore(requireContext())
}
Lee y observa los datos
- En
LetterListFragment
, dentro del métodoonViewCreated()
, debajo de la inicialización deSettingsDataStore
, conviertepreferenceFlow
enLivedata
usandoasLiveData
()
. Adjunta un observador y pasaviewLifecycleOwner
como el propietario.
SettingsDataStore.preferenceFlow.asLiveData().observe(viewLifecycleOwner, { })
- Dentro del observador, asigna los nuevos parámetros de configuración del diseño a la variable
isLinearLayoutManager
. Realiza una llamada a la funciónchooseLayout()
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
.
- En
LetterListFragment
, dentro de la funciónonOptionsItemSelected()
, al final del casoR.id.
action_switch_layout
, inicia la corrutina mediantelifecycleScope
. Dentro del bloquelaunch
, realiza una llamada alsaveLayoutToPreferencesStore()
y pasa elisLinearLayoutManager
y elcontext
.
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
}
- Ejecuta la app. Haz clic en la opción del menú para cambiar el diseño de la app.
- 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' y Run 'app' en Android Studio).
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.
- En
LetterListFragment
, dentro deonViewCreated()
, al final del observador depreferenceFlow
, debajo de la llamada achooseLayout()
. Vuelve a dibujar el menú llamando ainvalidateOptionsMenu()
en laactivity
.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
...
SettingsDataStore.preferenceFlow.asLiveData().observe(viewLifecycleOwner, { value ->
...
// Redraw the menu
activity?.invalidateOptionsMenu()
})
}
- Vuelve a ejecutar la app y cambia el diseño.
- Sal de la app y vuelve a iniciarla. Observa que el ícono de menú ahora se actualizó correctamente.
¡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 de Kotlin y Flow, 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 valorint
, usaintPreferencesKey()
. - Preferences DataStore proporciona una función
edit()
que actualiza los datos de forma transaccional en unDataStore
.