1. Antes de comenzar
Introducción
En esta unidad, aprendiste a usar SQL y Room para guardar datos de forma local en un dispositivo. SQL y Room son herramientas potentes. Sin embargo, en los casos en los que no necesites almacenar datos relacionales, DataStore puede proporcionar una solución simple. El componente DataStore de Jetpack es una excelente forma de almacenar conjuntos de datos pequeños y simples con baja sobrecarga. DataStore tiene dos implementaciones diferentes: Preferences DataStore
y Proto DataStore
.
Preferences DataStore
almacena pares clave-valor. Los valores pueden ser los tipos de datos básicos de Kotlin, comoString
,Boolean
yInteger
. No almacena conjuntos de datos complejos. No requiere un esquema predefinido. El caso de uso principal dePreferences Datastore
es almacenar las preferencias del usuario en su dispositivo.Proto DataStore
almacena tipos de datos personalizados. Requiere un esquema predefinido que asigne definiciones de proto con estructuras de objetos.
En este codelab, solo se aborda Preferences DataStore
, pero puedes leer más sobre Proto DataStore
en la documentación de DataStore.
Preferences DataStore
es una excelente manera de almacenar la configuración controlada por el usuario. En este codelab, aprenderás a implementar DataStore
para hacerlo.
Requisitos previos:
- Completa el curso Aspectos básicos de Android con Compose a través del codelab Cómo leer y actualizar datos con Room.
Requisitos
- Una computadora con acceso a Internet y Android Studio instalado
- Un dispositivo o emulador
- El código de partida para la app de Dessert Release
Qué compilarás
La app de Dessert Release muestra una lista de las versiones de Android. El ícono en la barra de la aplicación alterna el diseño entre una vista de cuadrícula y una de lista.
En su estado actual, la app no conserva la selección de diseño. Cuando cierras la app, no se guarda el diseño que elegiste y la configuración vuelve a la selección predeterminada. En este codelab, agregarás DataStore
a la app de Dessert Release y la usarás para almacenar una preferencia de selección de diseño.
2. Descarga el código de partida
Haz clic en el siguiente vínculo para descargar todo el código de este codelab:
Si lo prefieres, también puedes clonar el código de Dessert Release desde GitHub:
$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-dessert-release.git $ cd basic-android-kotlin-compose-training-dessert-release $ git checkout starter
- En Android Studio, abre la carpeta
basic-android-kotlin-compose-training-dessert-release
. - Abre el código de la app de Dessert Release en Android Studio.
3. Configura dependencias
Agrega lo siguiente a dependencies
en el archivo app/build.gradle.kts
:
implementation("androidx.datastore:datastore-preferences:1.0.0")
4. Implementa el repositorio de preferencias del usuario
- En el paquete
data
, crea una nueva clase llamadaUserPreferencesRepository
.
- En el constructor
UserPreferencesRepository
, define una propiedad de valor privado para representar una instancia del objetoDataStore
con un tipoPreferences
.
class UserPreferencesRepository(
private val dataStore: DataStore<Preferences>
){
}
DataStore
almacena pares clave-valor. Para acceder a un valor, debes definir una clave.
- Crea un
companion object
dentro de la claseUserPreferencesRepository
. - Usa la función
booleanPreferencesKey()
para definir una clave y pasarle el nombreis_linear_layout
. Al igual que los nombres de tablas de SQL, la clave debe usar un formato de guion bajo. Esta clave se usa para acceder a un valor booleano que indica si se debe mostrar el diseño lineal.
class UserPreferencesRepository(
private val dataStore: DataStore<Preferences>
){
private companion object {
val IS_LINEAR_LAYOUT = booleanPreferencesKey("is_linear_layout")
}
...
}
Escribe en DataStore
Para crear y modificar los valores en un DataStore
, pasa una lambda al método edit()
. La lambda recibe una instancia de MutablePreferences
, que puedes usar para actualizar valores en DataStore
. Todas las actualizaciones dentro de esta lambda se ejecutan como una sola transacción. Dicho de otro modo, la actualización es atómica: todo ocurre al mismo tiempo. Este tipo de actualización evita una situación en la que algunos valores se actualizan, pero otros no.
- Crea una función de suspensión y llámala
saveLayoutPreference()
. - En la función
saveLayoutPreference()
, llama al métodoedit()
en el objetodataStore
.
suspend fun saveLayoutPreference(isLinearLayout: Boolean) {
dataStore.edit {
}
}
- Para que tu código sea más legible, define un nombre para
MutablePreferences
que se proporciona en el cuerpo de la lambda. Usa esa propiedad para configurar un valor con la clave que definiste y el booleano que se pasó a la funciónsaveLayoutPreference()
.
suspend fun saveLayoutPreference(isLinearLayout: Boolean) {
dataStore.edit { preferences ->
preferences[IS_LINEAR_LAYOUT] = isLinearLayout
}
}
Lee desde DataStore
Ahora que creaste una forma de escribir isLinearLayout
en dataStore
, sigue estos pasos para leerla:
- Crea una propiedad en
UserPreferencesRepository
del tipoFlow<Boolean>
llamadaisLinearLayout
.
val isLinearLayout: Flow<Boolean> =
- Puedes usar la propiedad
DataStore.data
para exponer los valores deDataStore
. ConfiguraisLinearLayout
en la propiedaddata
del objetoDataStore
.
val isLinearLayout: Flow<Boolean> = dataStore.data
La propiedad data
es un Flow
de objetos Preferences
. El objeto Preferences
contiene todos los pares clave-valor en DataStore. Cada vez que se actualizan los datos de DataStore, se emite un nuevo objeto Preferences
en Flow
.
- Usa la función de asignación para convertir
Flow<Preferences>
enFlow<Boolean>
.
Esta función acepta una expresión lambda con el objeto Preferences
actual como parámetro. Puedes especificar la clave que definiste antes para obtener la preferencia de diseño. Ten en cuenta que es posible que el valor no exista si aún no se llamó a saveLayoutPreference
, por lo que también debes proporcionar un valor predeterminado.
- Especifica
true
para que se establezca la vista de diseño lineal como predeterminada.
val isLinearLayout: Flow<Boolean> = dataStore.data.map { preferences ->
preferences[IS_LINEAR_LAYOUT] ?: true
}
Manejo de excepciones
Cada vez que interactúas con el sistema de archivos en un dispositivo, es posible que falle algo. Por ejemplo, puede que un archivo no exista o que el disco esté lleno o desactivado. A medida que DataStore
lee y escribe datos de archivos, pueden ocurrir IOExceptions
cuando se accede a DataStore
. Usa el operador catch{}
para detectar excepciones y controlar estas fallas.
- En el objeto complementario, implementa una propiedad de cadena inmutable
TAG
para usarla en el registro.
private companion object {
val IS_LINEAR_LAYOUT = booleanPreferencesKey("is_linear_layout")
const val TAG = "UserPreferencesRepo"
}
Preferences DataStore
arroja unaIOException
cuando se encuentra un error durante la lectura de los datos. En el bloque de inicializaciónisLinearLayout
, antes demap()
, usa el operadorcatch{}
para detectar laIOException
.
val isLinearLayout: Flow<Boolean> = dataStore.data
.catch {}
.map { preferences ->
preferences[IS_LINEAR_LAYOUT] ?: true
}
- En el bloque catch, si hay una
IOexception
, registra el error y emiteemptyPreferences()
. Si se muestra un tipo diferente de excepción, es preferible que se vuelva a arrojar. Si se emiteemptyPreferences()
si hay un error, la función de asignación aún puede asignarse al valor predeterminado.
val isLinearLayout: Flow<Boolean> = dataStore.data
.catch {
if(it is IOException) {
Log.e(TAG, "Error reading preferences.", it)
emit(emptyPreferences())
} else {
throw it
}
}
.map { preferences ->
preferences[IS_LINEAR_LAYOUT] ?: true
}
5. Inicializa DataStore
En este codelab, debes controlar la inserción de dependencias de forma manual. Por lo tanto, debes proporcionar manualmente la clase Preferences DataStore
con UserPreferencesRepository
. Sigue estos pasos para insertar DataStore
en UserPreferencesRepository
.
- Busca el paquete
dessertrelease
. - Dentro de este directorio, crea una nueva clase llamada
DessertReleaseApplication
y, luego, implementa la claseApplication
. Este es el contenedor para tu DataStore.
class DessertReleaseApplication: Application() {
}
- Dentro del archivo
DessertReleaseApplication.kt
, pero fuera de la claseDessertReleaseApplication
, declara unprivate const val
llamadoLAYOUT_PREFERENCE_NAME
. - Asigna a la variable
LAYOUT_PREFERENCE_NAME
el valor de cadenalayout_preferences
, que puedes usar como nombre delPreferences Datastore
del que creaste una instancia en el siguiente paso.
private const val LAYOUT_PREFERENCE_NAME = "layout_preferences"
- Aún fuera del cuerpo de la clase
DessertReleaseApplication
, pero en el archivoDessertReleaseApplication.kt
, crea una propiedad de valor privado de tipoDataStore<Preferences>
llamadaContext.dataStore
con el delegadopreferencesDataStore
. PasaLAYOUT_PREFERENCE_NAME
para el parámetroname
del delegadopreferencesDataStore
.
private const val LAYOUT_PREFERENCE_NAME = "layout_preferences"
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
name = LAYOUT_PREFERENCE_NAME
)
- Dentro del cuerpo de la clase
DessertReleaseApplication
, crea una instancialateinit var
deUserPreferencesRepository
.
private const val LAYOUT_PREFERENCE_NAME = "layout_preferences"
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
name = LAYOUT_PREFERENCE_NAME
)
class DessertReleaseApplication: Application() {
lateinit var userPreferencesRepository: UserPreferencesRepository
}
- Anula el método
onCreate()
.
private const val LAYOUT_PREFERENCE_NAME = "layout_preferences"
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
name = LAYOUT_PREFERENCE_NAME
)
class DessertReleaseApplication: Application() {
lateinit var userPreferencesRepository: UserPreferencesRepository
override fun onCreate() {
super.onCreate()
}
}
- Dentro del método
onCreate()
, inicializauserPreferencesRepository
construyendo unUserPreferencesRepository
condataStore
como su parámetro.
private const val LAYOUT_PREFERENCE_NAME = "layout_preferences"
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
name = LAYOUT_PREFERENCE_NAME
)
class DessertReleaseApplication: Application() {
lateinit var userPreferencesRepository: UserPreferencesRepository
override fun onCreate() {
super.onCreate()
userPreferencesRepository = UserPreferencesRepository(dataStore)
}
}
- Agrega la siguiente línea dentro de la etiqueta
<application>
en el archivoAndroidManifest.xml
.
<application
android:name=".DessertReleaseApplication"
...
</application>
Este enfoque define la clase DessertReleaseApplication
como punto de entrada de la app. El propósito de este código es inicializar las dependencias definidas en la clase DessertReleaseApplication
antes de iniciar MainActivity
.
6. Usa UserPreferencesRepository
Proporciona el repositorio al ViewModel
Ahora que UserPreferencesRepository
está disponible a través de la inserción de dependencias, puedes usarlo en DessertReleaseViewModel
.
- En el
DessertReleaseViewModel
, crea una propiedadUserPreferencesRepository
como parámetro de constructor.
class DessertReleaseViewModel(
private val userPreferencesRepository: UserPreferencesRepository
) : ViewModel() {
...
}
- Dentro del objeto complementario de
ViewModel
, en el bloqueviewModelFactory initializer
, obtén una instancia deDessertReleaseApplication
con el siguiente código.
...
companion object {
val Factory: ViewModelProvider.Factory = viewModelFactory {
initializer {
val application = (this[APPLICATION_KEY] as DessertReleaseApplication)
...
}
}
}
}
- Crea una instancia de
DessertReleaseViewModel
y pasa eluserPreferencesRepository
.
...
companion object {
val Factory: ViewModelProvider.Factory = viewModelFactory {
initializer {
val application = (this[APPLICATION_KEY] as DessertReleaseApplication)
DessertReleaseViewModel(application.userPreferencesRepository)
}
}
}
}
Ahora, ViewModel puede acceder a UserPreferencesRepository
. Los siguientes pasos son usar las capacidades de lectura y escritura del UserPreferencesRepository
que implementaste antes.
Cómo almacenar la preferencia de diseño
- Edita la función
selectLayout()
enDessertReleaseViewModel
para acceder al repositorio de preferencias y actualizar la preferencia de diseño. - Recuerda que escribir en
DataStore
se realiza de forma asíncrona con una funciónsuspend
. Inicia una nueva corrutina para llamar a la funciónsaveLayoutPreference()
del repositorio de preferencias.
fun selectLayout(isLinearLayout: Boolean) {
viewModelScope.launch {
userPreferencesRepository.saveLayoutPreference(isLinearLayout)
}
}
Cómo leer la preferencia de diseño
En esta sección, refactorizarás el uiState: StateFlow
existente en ViewModel
para reflejar el isLinearLayout: Flow
del repositorio.
- Borra el código que inicializa la propiedad
uiState
enMutableStateFlow(DessertReleaseUiState)
.
val uiState: StateFlow<DessertReleaseUiState> =
La preferencia de diseño lineal del repositorio tiene dos valores posibles, true o false, con el formato de Flow<Boolean>
. Este valor debe asignarse a un estado de IU.
- Establece
StateFlow
en el resultado de la transformación de la colecciónmap()
a la que se llama enisLinearLayout Flow
.
val uiState: StateFlow<DessertReleaseUiState> =
userPreferencesRepository.isLinearLayout.map { isLinearLayout ->
}
- Muestra una instancia de la clase de datos
DessertReleaseUiState
y pasa elisLinearLayout Boolean
. La pantalla usa este estado de la IU para determinar las cadenas y los íconos correctos que se mostrarán.
val uiState: StateFlow<DessertReleaseUiState> =
userPreferencesRepository.isLinearLayout.map { isLinearLayout ->
DessertReleaseUiState(isLinearLayout)
}
UserPreferencesRepository.isLinearLayout
es un Flow
que es frío. Sin embargo, para proporcionar el estado a la IU, es mejor usar un flujo caliente, como StateFlow
, de modo que el estado siempre esté disponible de inmediato para la IU.
- Usa la función
stateIn()
para convertir unFlow
en unStateFlow
. - La función
stateIn()
acepta tres parámetros:scope
,started
yinitialValue
. PasaviewModelScope
,SharingStarted.WhileSubscribed(5_000)
yDessertReleaseUiState()
para estos parámetros, respectivamente.
val uiState: StateFlow<DessertReleaseUiState> =
userPreferencesRepository.isLinearLayout.map { isLinearLayout ->
DessertReleaseUiState(isLinearLayout)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = DessertReleaseUiState()
)
- Inicia la app. Observa que puedes hacer clic en el ícono para alternar entre un diseño de cuadrícula y uno lineal.
¡Felicitaciones! Agregaste Preferences DataStore
a tu app de forma correcta para guardar la preferencia de diseño del usuario.
7. Obtén el código de la solución
Para descargar el código del codelab terminado, puedes usar estos comandos de git:
$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-dessert-release.git $ cd basic-android-kotlin-compose-training-dessert-release $ git checkout main
También puedes descargar el repositorio como un archivo ZIP, descomprimirlo y abrirlo en Android Studio.
Si deseas ver el código de la solución, puedes hacerlo en GitHub.