Guarda tus preferencias de forma local con DataStore

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, como String, Boolean y Integer. No almacena conjuntos de datos complejos. No requiere un esquema predefinido. El caso de uso principal de Preferences 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:

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.

b6e4bd0e50915b81.png 24a261db4cf2c6b8.png

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:

Descargar ZIP

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
  1. En Android Studio, abre la carpeta basic-android-kotlin-compose-training-dessert-release.
  2. 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

  1. En el paquete data, crea una nueva clase llamada UserPreferencesRepository.

c4c2e90902898001.png

  1. En el constructor UserPreferencesRepository, define una propiedad de valor privado para representar una instancia del objeto DataStore con un tipo Preferences.
class UserPreferencesRepository(
    private val dataStore: DataStore<Preferences>
){
}

DataStore almacena pares clave-valor. Para acceder a un valor, debes definir una clave.

  1. Crea un companion object dentro de la clase UserPreferencesRepository.
  2. Usa la función booleanPreferencesKey() para definir una clave y pasarle el nombre is_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.

  1. Crea una función de suspensión y llámala saveLayoutPreference().
  2. En la función saveLayoutPreference(), llama al método edit() en el objeto dataStore.
suspend fun saveLayoutPreference(isLinearLayout: Boolean) {
    dataStore.edit {

    }
}
  1. 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ón saveLayoutPreference().
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:

  1. Crea una propiedad en UserPreferencesRepository del tipo Flow<Boolean> llamada isLinearLayout.
val isLinearLayout: Flow<Boolean> =
  1. Puedes usar la propiedad DataStore.data para exponer los valores de DataStore. Configura isLinearLayout en la propiedad data del objeto DataStore.
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.

  1. Usa la función de asignación para convertir Flow<Preferences> en Flow<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.

  1. 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.

  1. 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"
}
  1. Preferences DataStore arroja una IOException cuando se encuentra un error durante la lectura de los datos. En el bloque de inicialización isLinearLayout, antes de map(), usa el operador catch{} para detectar la IOException.
val isLinearLayout: Flow<Boolean> = dataStore.data
    .catch {}
    .map { preferences ->
        preferences[IS_LINEAR_LAYOUT] ?: true
    }
  1. En el bloque catch, si hay una IOexception, registra el error y emite emptyPreferences(). Si se muestra un tipo diferente de excepción, es preferible que se vuelva a arrojar. Si se emite emptyPreferences() 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.

  1. Busca el paquete dessertrelease.
  2. Dentro de este directorio, crea una nueva clase llamada DessertReleaseApplication y, luego, implementa la clase Application. Este es el contenedor para tu DataStore.
class DessertReleaseApplication: Application() {
}
  1. Dentro del archivo DessertReleaseApplication.kt, pero fuera de la clase DessertReleaseApplication, declara un private const val llamado LAYOUT_PREFERENCE_NAME.
  2. Asigna a la variable LAYOUT_PREFERENCE_NAME el valor de cadena layout_preferences, que puedes usar como nombre del Preferences Datastore del que creaste una instancia en el siguiente paso.
private const val LAYOUT_PREFERENCE_NAME = "layout_preferences"
  1. Aún fuera del cuerpo de la clase DessertReleaseApplication, pero en el archivo DessertReleaseApplication.kt, crea una propiedad de valor privado de tipo DataStore<Preferences> llamada Context.dataStore con el delegado preferencesDataStore. Pasa LAYOUT_PREFERENCE_NAME para el parámetro name del delegado preferencesDataStore.
private const val LAYOUT_PREFERENCE_NAME = "layout_preferences"
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
    name = LAYOUT_PREFERENCE_NAME
)
  1. Dentro del cuerpo de la clase DessertReleaseApplication, crea una instancia lateinit var de UserPreferencesRepository.
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
}
  1. 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()
    }
}
  1. Dentro del método onCreate(), inicializa userPreferencesRepository construyendo un UserPreferencesRepository con dataStore 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)
    }
}
  1. Agrega la siguiente línea dentro de la etiqueta <application> en el archivo AndroidManifest.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.

  1. En el DessertReleaseViewModel, crea una propiedad UserPreferencesRepository como parámetro de constructor.
class DessertReleaseViewModel(
    private val userPreferencesRepository: UserPreferencesRepository
) : ViewModel() {
    ...
}
  1. Dentro del objeto complementario de ViewModel, en el bloque viewModelFactory initializer, obtén una instancia de DessertReleaseApplication con el siguiente código.
    ...
    companion object {
        val Factory: ViewModelProvider.Factory = viewModelFactory {
            initializer {
                val application = (this[APPLICATION_KEY] as DessertReleaseApplication)
                ...
            }
        }
    }
}
  1. Crea una instancia de DessertReleaseViewModel y pasa el userPreferencesRepository.
    ...
    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

  1. Edita la función selectLayout() en DessertReleaseViewModel para acceder al repositorio de preferencias y actualizar la preferencia de diseño.
  2. Recuerda que escribir en DataStore se realiza de forma asíncrona con una función suspend. Inicia una nueva corrutina para llamar a la función saveLayoutPreference() 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.

  1. Borra el código que inicializa la propiedad uiState en MutableStateFlow(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.

  1. Establece StateFlow en el resultado de la transformación de la colección map() a la que se llama en isLinearLayout Flow.
val uiState: StateFlow<DessertReleaseUiState> =
    userPreferencesRepository.isLinearLayout.map { isLinearLayout ->
}
  1. Muestra una instancia de la clase de datos DessertReleaseUiState y pasa el isLinearLayout 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.

  1. Usa la función stateIn() para convertir un Flow en un StateFlow.
  2. La función stateIn() acepta tres parámetros: scope, started y initialValue. Pasa viewModelScope, SharingStarted.WhileSubscribed(5_000) y DessertReleaseUiState() 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()
    )
  1. Inicia la app. Observa que puedes hacer clic en el ícono para alternar entre un diseño de cuadrícula y uno lineal.

b6e4bd0e50915b81.png 24a261db4cf2c6b8.png

¡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.

Descargar ZIP

Si deseas ver el código de la solución, puedes hacerlo en GitHub.