Aspectos básicos de Android Kotlin 06.3: Cómo usar LiveData para controlar los estados de los botones

Este codelab es parte del curso Conceptos básicos de Kotlin para Android. Aprovecharás al máximo este curso si trabajas con los codelabs en secuencia. Todos los codelabs del curso se detallan en la página de destino de codelabs sobre los aspectos básicos de Kotlin para Android.

Introducción

En este codelab, se resume cómo usar ViewModel y fragmentos juntos para implementar la navegación. Recuerda que el objetivo es colocar la lógica de when para navegar a ViewModel, pero definir las rutas de acceso en los fragmentos y del archivo de navegación. Para lograr este objetivo, debes usar modelos de vista, fragmentos, LiveData y observadores.

El codelab finaliza con una forma ingeniosa de realizar un seguimiento de los estados de los botones con un código mínimo, de modo que cada uno esté habilitado y se pueda hacer clic solo cuando el usuario tenga que presionarlo.

Conocimientos que ya deberías tener

Debes estar familiarizado con lo siguiente:

  • Compilar una interfaz de usuario básica (IU) mediante una actividad, fragmentos y vistas
  • La navegación entre fragmentos y el uso de safeArgs para pasar datos entre fragmentos
  • Visualiza modelos, fábricas de modelos, transformaciones y LiveData y sus observadores.
  • Cómo crear una base de datos Room, crear un objeto de acceso a datos (DAO) y definir entidades
  • Cómo usar corrutinas para las interacciones de bases de datos y otras tareas de larga duración

Qué aprenderás

  • Cómo actualizar un registro existente de calidad del sueño en la base de datos
  • Cómo usar LiveData para realizar un seguimiento de los estados de los botones
  • Cómo mostrar una barra de notificaciones en respuesta a un evento

Actividades

  • Extiende la app TrackMySleepQuality para recopilar una calificación de calidad, agregarla a la base de datos y mostrar el resultado.
  • Usa LiveData para activar la visualización de una barra de notificaciones.
  • Usa LiveData para habilitar e inhabilitar botones.

En este codelab, compilarás la grabación de la calidad del sueño y la IU finalizada de la app de TrackMySleepQuality.

La app tiene dos pantallas, representadas por fragmentos, como se muestra en la siguiente figura.

La primera pantalla, que se muestra a la izquierda, tiene botones para iniciar y detener el seguimiento. La pantalla muestra todos los datos de sueño del usuario. El botón Borrar borra de forma permanente todos los datos que la app recopiló para el usuario.

En la segunda pantalla, que se muestra a la derecha, se selecciona una calificación de calidad del sueño. En la app, la calificación se representa numéricamente. Para fines de desarrollo, la app muestra los íconos de rostro y sus equivalentes numéricos.

El flujo del usuario es el siguiente:

  • El usuario abre la app y aparece la pantalla de seguimiento del sueño.
  • El usuario presiona el botón Iniciar. Esto registra la hora de inicio y la muestra. El botón Iniciar está inhabilitado y está habilitado el botón Detener.
  • El usuario presiona el botón Detener. Se grabará la hora de finalización y se abrirá la pantalla de calidad del sueño.
  • El usuario selecciona un ícono de calidad del sueño. La pantalla se cerrará, y la pantalla de seguimiento mostrará la hora de finalización y la calidad del sueño. El botón Detener está inhabilitado y habilitado el botón Iniciar. La app está lista para otra noche.
  • Se habilita el botón Borrar cuando la base de datos tiene datos. Cuando el usuario presiona el botón Borrar, se borran todos sus datos sin recursos, y no se muestra el mensaje "¿Estás seguro?".

Esta app usa una arquitectura simplificada, como se muestra a continuación en el contexto de la arquitectura completa. La app usa solo los siguientes componentes:

  • Controlador de IU
  • Ver el modelo y LiveData
  • Una base de datos de Room

En este codelab, se supone que sabes cómo implementar la navegación mediante fragmentos y el archivo de navegación. Para ahorrarte trabajo, se proporciona una buena cantidad de este código.

Paso 1: Inspecciona el código

  1. Para comenzar, continúa con tu propio código desde el final del último codelab o descarga el código de inicio.
  2. En tu código de inicio, inspecciona SleepQualityFragment. Esta clase aumenta el diseño, obtiene la aplicación y muestra binding.root.
  3. Abre navigation.xml en el editor de diseño. Verás que hay una ruta de navegación de SleepTrackerFragment a SleepQualityFragment y de regreso a SleepQualityFragment. SleepTrackerFragment


  4. Inspecciona el código de navigation.xml. En particular, busca el elemento <argument> con el nombre sleepNightKey.

    Cuando el usuario pase de SleepTrackerFragment a SleepQualityFragment,, la app pasará un sleepNightKey a SleepQualityFragment para la noche que deba actualizarse.

Paso 2: Agrega la navegación para hacer un seguimiento de la calidad del sueño

El gráfico de navegación ya incluye las rutas de acceso de SleepTrackerFragment a SleepQualityFragment y viceversa. Sin embargo, aún no se codifican los controladores de clics que implementan la navegación de un fragmento al siguiente. Ahora, agrega ese código a ViewModel.

En el controlador de clics, configuras un elemento LiveData que cambia cuando quieres que la app navegue a un destino diferente. El fragmento observa este LiveData. Cuando cambian los datos, el fragmento navega al destino y le indica al modelo de vista que lo hizo, lo que restablece la variable de estado.

  1. Abre SleepTrackerViewModel. Debes agregar una navegación para que, cuando el usuario presione el botón Detener, la app navegue a SleepQualityFragment para recopilar una calificación de calidad.
  2. En SleepTrackerViewModel, crea un LiveData que cambie cuando quieras que la app navegue a SleepQualityFragment. Usa el encapsulamiento para exponer solo una versión de LiveData que se pueda obtener en ViewModel.

    Puedes colocar este código en cualquier lugar del nivel superior del cuerpo de la clase.
private val _navigateToSleepQuality = MutableLiveData<SleepNight>()

val navigateToSleepQuality: LiveData<SleepNight>
   get() = _navigateToSleepQuality
  1. Agrega una función doneNavigating() que restablezca la variable que activa la navegación.
fun doneNavigating() {
   _navigateToSleepQuality.value = null
}
  1. En el controlador de clics del botón Stop, onStopTracking(), activa la navegación hacia SleepQualityFragment. Configura la variable _navigateToSleepQuality al final de la función como último elemento dentro del bloque launch{}. Ten en cuenta que esta variable está configurada en night. Cuando esta variable tiene un valor, la app navega a SleepQualityFragment y pasa la noche.
_navigateToSleepQuality.value = oldNight
  1. SleepTrackerFragment necesita observar _navigateToSleepQuality para que la app sepa cuándo navegar. En el SleepTrackerFragment, en onCreateView(), agrega un observador para navigateToSleepQuality(). Ten en cuenta que la importación es ambigua y deberás importar androidx.lifecycle.Observer.
sleepTrackerViewModel.navigateToSleepQuality.observe(this, Observer {
})

  1. Dentro del bloque de observador, navega y pasa el ID de la noche actual, y luego llama a doneNavigating(). Si la importación es ambigua, importa androidx.navigation.fragment.findNavController.
night ->
night?.let {
   this.findNavController().navigate(
           SleepTrackerFragmentDirections
                   .actionSleepTrackerFragmentToSleepQualityFragment(night.nightId))
   sleepTrackerViewModel.doneNavigating()
}
  1. Compila y ejecuta la app. Presiona Iniciar y, luego, Detener, que te llevará a la pantalla SleepQualityFragment. Para volver, usa el botón Atrás del sistema.

En esta tarea, registrarás la calidad del sueño y navegarás de vuelta al fragmento de seguimiento del sueño. La pantalla debería actualizarse automáticamente para mostrarle el valor actualizado al usuario. Debes crear un ViewModel y un ViewModelFactory, y actualizar la SleepQualityFragment.

Paso 1: Crea un ViewModel y un ViewModelFactory

  1. En el paquete sleepquality, crea o abre SleepQualityViewModel.kt.
  2. Crea una clase SleepQualityViewModel que tome un sleepNightKey y una base de datos como argumentos. Al igual que con el SleepTrackerViewModel, debes pasar el database de la fábrica. También debes pasar el sleepNightKey desde la navegación.
class SleepQualityViewModel(
       private val sleepNightKey: Long = 0L,
       val database: SleepDatabaseDao) : ViewModel() {
}
  1. Dentro de la clase SleepQualityViewModel, define un Job y un uiScope, y anula onCleared().
private val viewModelJob = Job()
private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)

override fun onCleared() {
   super.onCleared()
   viewModelJob.cancel()
}
  1. Para volver a SleepTrackerFragment con el mismo patrón que se indicó anteriormente, declara _navigateToSleepTracker. Implementa navigateToSleepTracker y doneNavigating().
private val _navigateToSleepTracker = MutableLiveData<Boolean?>()

val navigateToSleepTracker: LiveData<Boolean?>
   get() = _navigateToSleepTracker

fun doneNavigating() {
   _navigateToSleepTracker.value = null
}
  1. Crea un controlador de clics, onSetSleepQuality(), para todas las imágenes de calidad de sueño que quieras usar.

    Usa el mismo patrón de corrutina que en el codelab anterior:
  • Inicia una corrutina en el uiScope y cambia al despachador de E/S.
  • Obtén tonight mediante sleepNightKey.
  • Establece la calidad del sueño.
  • Actualiza la base de datos.
  • Activar la navegación

Observa que la muestra de código que aparece a continuación hace todo el trabajo en el controlador de clics, en lugar de restar la operación de base de datos en el contexto diferente.

fun onSetSleepQuality(quality: Int) {
        uiScope.launch {
            // IO is a thread pool for running operations that access the disk, such as
            // our Room database.
            withContext(Dispatchers.IO) {
                val tonight = database.get(sleepNightKey) ?: return@withContext
                tonight.sleepQuality = quality
                database.update(tonight)
            }

            // Setting this state variable to true will alert the observer and trigger navigation.
            _navigateToSleepTracker.value = true
        }
    }
  1. En el paquete sleepquality, crea o abre SleepQualityViewModelFactory.kt y agrega la clase SleepQualityViewModelFactory, como se muestra a continuación. Esta clase usa una versión del mismo código estándar que viste antes. Inspecciona el código antes de continuar.
class SleepQualityViewModelFactory(
       private val sleepNightKey: Long,
       private val dataSource: SleepDatabaseDao) : ViewModelProvider.Factory {
   @Suppress("unchecked_cast")
   override fun <T : ViewModel?> create(modelClass: Class<T>): T {
       if (modelClass.isAssignableFrom(SleepQualityViewModel::class.java)) {
           return SleepQualityViewModel(sleepNightKey, dataSource) as T
       }
       throw IllegalArgumentException("Unknown ViewModel class")
   }
}

Paso 2: Actualiza el SleepQualityFragment

  1. Abre SleepQualityFragment.kt.
  2. En onCreateView(), después de obtener la application, debes obtener la arguments que vino con la navegación. Estos argumentos se encuentran en SleepQualityFragmentArgs. Debes extraerlas del paquete.
val arguments = SleepQualityFragmentArgs.fromBundle(arguments!!)
  1. A continuación, obtén la dataSource.
val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao
  1. Crea una fábrica y pasa los objetos dataSource y sleepNightKey.
val viewModelFactory = SleepQualityViewModelFactory(arguments.sleepNightKey, dataSource)
  1. Obtén una referencia de ViewModel.
val sleepQualityViewModel =
       ViewModelProviders.of(
               this, viewModelFactory).get(SleepQualityViewModel::class.java)
  1. Agrega el ViewModel al objeto de vinculación. (Si ves un error con el objeto de vinculación, ignóralo por ahora).
binding.sleepQualityViewModel = sleepQualityViewModel
  1. Agrega el observador. Cuando se te solicite, importa androidx.lifecycle.Observer.
sleepQualityViewModel.navigateToSleepTracker.observe(this, Observer {
   if (it == true) { // Observed state is true.
       this.findNavController().navigate(
               SleepQualityFragmentDirections.actionSleepQualityFragmentToSleepTrackerFragment())
       sleepQualityViewModel.doneNavigating()
   }
})

Paso 3: Actualiza el archivo de diseño y ejecuta la app

  1. Abre el archivo de diseño fragment_sleep_quality.xml. En el bloque <data>, agrega una variable para SleepQualityViewModel.
 <data>
       <variable
           name="sleepQualityViewModel"
           type="com.example.android.trackmysleepquality.sleepquality.SleepQualityViewModel" />
   </data>
  1. Para cada una de las seis imágenes de calidad de sueño, agrega un controlador de clics como el que se muestra a continuación. Relaciona la calificación de calidad con la imagen.
android:onClick="@{() -> sleepQualityViewModel.onSetSleepQuality(5)}"
  1. Limpia y vuelve a compilar tu proyecto. Esto debería resolver cualquier error con el objeto de vinculación. De lo contrario, borra la caché (File > Invalidate Caches / Restart) y vuelve a compilar la app.

¡Felicitaciones! Acabas de compilar una app de base de datos Room completa mediante corrutinas.

Ahora tu app funciona perfectamente. El usuario puede presionar Iniciar y Detener tantas veces como lo desee. Cuando el usuario presiona Detener, puede ingresar una calidad de sueño. Cuando el usuario presiona Borrar, se borran todos los datos en segundo plano de forma silenciosa. Sin embargo, todos los botones siempre están habilitados y se pueden hacer clic, lo cual no interrumpe la app, pero permite que los usuarios creen noches de sueño incompletas.

En esta última tarea, aprenderá a usar mapas de transformación para administrar la visibilidad de los botones a fin de que los usuarios solo puedan tomar la decisión correcta. Puedes usar un método similar para mostrar un mensaje amigable después de que se hayan borrado todos los datos.

Paso 1: Actualiza los estados del botón

La idea es establecer el estado del botón para que, al comienzo, solo esté habilitado el botón Iniciar, lo que significa que se puede hacer clic en él.

Después de que el usuario presiona Iniciar, se habilita el botón Detener y no se inicia Comenzar. El botón Borrar solo está habilitado cuando hay datos en la base de datos.

  1. Abre el archivo de diseño fragment_sleep_tracker.xml.
  2. Agrega la propiedad android:enabled a cada botón. La propiedad android:enabled es un valor booleano que indica si el botón está habilitado o no. (Se puede presionar un botón habilitado; no es posible presionar un botón inhabilitado) Dale a la propiedad el valor de una variable de estado que definirás en un momento.

start_button:

android:enabled="@{sleepTrackerViewModel.startButtonVisible}"

stop_button:

android:enabled="@{sleepTrackerViewModel.stopButtonVisible}"

clear_button:

android:enabled="@{sleepTrackerViewModel.clearButtonVisible}"
  1. Abre SleepTrackerViewModel y crea las tres variables correspondientes. Asigna a cada variable una transformación que la pruebe.
  • Se debe habilitar el botón Start cuando tonight esté null.
  • Se debe habilitar el botón Stop cuando tonight no sea null.
  • El botón Borrar solo debe habilitarse si nights, por lo tanto, la base de datos contiene noches de sueño.
val startButtonVisible = Transformations.map(tonight) {
   it == null
}
val stopButtonVisible = Transformations.map(tonight) {
   it != null
}
val clearButtonVisible = Transformations.map(nights) {
   it?.isNotEmpty()
}
  1. Ejecuta la app y experimenta con los botones.

Paso 2: Usa una barra de notificaciones para notificar al usuario

Después de que el usuario borre la base de datos, muéstrale una confirmación mediante el widget Snackbar. Una barra de notificaciones proporciona comentarios breves sobre una operación a través de un mensaje en la parte inferior de la pantalla. Una barra de notificaciones desaparece después de un tiempo de espera, después de la interacción de un usuario en cualquier parte de la pantalla o después de que desliza la barra de notificaciones fuera de la pantalla.

Mostrar la barra de notificaciones es una tarea de IU que debería realizarse en el fragmento. La ViewModel muestra la barra de notificaciones. Para configurar y activar una barra de notificaciones cuando se borran los datos, puedes usar la misma técnica que se utiliza para activar la navegación.

  1. En SleepTrackerViewModel, crea el evento encapsulado.
private var _showSnackbarEvent = MutableLiveData<Boolean>()

val showSnackBarEvent: LiveData<Boolean>
   get() = _showSnackbarEvent
  1. Luego, implementa doneShowingSnackbar().
fun doneShowingSnackbar() {
   _showSnackbarEvent.value = false
}
  1. En SleepTrackerFragment, en onCreateView(), agrega un observador:
sleepTrackerViewModel.showSnackBarEvent.observe(this, Observer { })
  1. Dentro del bloque de observador, muestra la barra de notificaciones y restablece el evento de inmediato.
   if (it == true) { // Observed state is true.
       Snackbar.make(
               activity!!.findViewById(android.R.id.content),
               getString(R.string.cleared_message),
               Snackbar.LENGTH_SHORT // How long to display the message.
       ).show()
       sleepTrackerViewModel.doneShowingSnackbar()
   }
  1. En SleepTrackerViewModel, activa el evento en el método onClear(). Para ello, establece el valor del evento en true dentro del bloque launch:
_showSnackbarEvent.value = true
  1. Compila y ejecuta tu app

Proyecto de Android Studio: TrackMySleepQualityFinal

Implementar el seguimiento de la calidad del sueño en esta app es como tocar una pieza de música familiar en una nueva clave. Si bien los detalles cambian, el patrón subyacente de lo que hiciste en codelabs anteriores en esta lección sigue siendo el mismo. Tener en cuenta estos patrones hace que la codificación sea mucho más rápida, ya que puedes reutilizar el código de apps existentes. Estos son algunos de los patrones que se usan hasta ahora en este curso:

  • Crea un ViewModel y un ViewModelFactory, y configura una fuente de datos.
  • Activar la navegación Para separar los problemas, coloca el controlador de clics en el modelo de vista y la navegación en el fragmento.
  • Usa el encapsulamiento con LiveData para realizar un seguimiento de los cambios de estado y responder a ellos.
  • Usa transformaciones con LiveData.
  • Crea una base de datos singleton.
  • Configura corrutinas para operaciones de bases de datos.

Cómo activar la navegación

Puedes definir posibles rutas de navegación entre fragmentos en un archivo de navegación. Existen diferentes maneras de activar la navegación de un fragmento al siguiente. Estos incluyen los siguientes:

  • Define los controladores de onClick para activar la navegación a un fragmento de destino.
  • Como alternativa, puedes habilitar la navegación de un fragmento al siguiente:
  • Define un valor de LiveData para registrar si debe ocurrir una navegación.
  • Adjunta un observador a ese valor de LiveData.
  • Tu código cambiará ese valor cada vez que la activación deba activarse o esté completa.

Cómo configurar el atributo android:enabled

  • El atributo android:enabled se define en TextView y lo heredan todas las subclases, incluido Button.
  • El atributo android:enabled determina si se habilita o no un View. El significado de "habilitado" varía según la subclase. Por ejemplo, un elemento EditText no habilitado evita que el usuario edite el texto contenido, y un elemento Button no habilitado impide que presione el botón.
  • El atributo enabled no es lo mismo que el atributo visibility.
  • Puedes usar mapas de transformación para establecer el valor del atributo enabled de los botones según el estado de otro objeto o variable.

Otros puntos que se abordan en este codelab:

  • Si quieres activar notificaciones para el usuario, puedes usar la misma técnica que utilizas para activar la navegación.
  • Puedes usar un Snackbar para notificar al usuario.

Curso de Udacity:

Documentación para desarrolladores de Android:

En esta sección, se enumeran las posibles tareas para los alumnos que trabajan con este codelab como parte de un curso que dicta un instructor. Depende del instructor hacer lo siguiente:

  • Si es necesario, asigna la tarea.
  • Informa a los alumnos cómo enviar los deberes.
  • Califica las tareas.

Los instructores pueden usar estas sugerencias lo poco o lo que quieran, y deben asignar cualquier otra tarea que consideren apropiada.

Si estás trabajando en este codelab por tu cuenta, usa estas tareas para poner a prueba tus conocimientos.

Responde estas preguntas

Pregunta 1

Una forma de permitir que tu app active la navegación de un fragmento al siguiente es usar un valor LiveData para indicar si activar o no la navegación.

¿Cuáles son los pasos para usar un valor de LiveData, llamado gotoBlueFragment, a fin de activar la navegación del fragmento rojo al azul? Selecciona todas las opciones que correspondan:

  • En ViewModel, define el valor LiveData gotoBlueFragment.
  • En RedFragment, observa el valor gotoBlueFragment. Implementa el código observe{} para navegar a BlueFragment cuando corresponda y, luego, restablece el valor de gotoBlueFragment a fin de indicar que la navegación está completa.
  • Asegúrate de que tu código establezca la variable gotoBlueFragment en el valor que activa la navegación cada vez que la app necesite pasar de RedFragment a BlueFragment.
  • Asegúrate de que tu código defina un controlador onClick para el View en el que el usuario hace clic a fin de navegar a BlueFragment, donde el controlador onClick observa el valor goToBlueFragment.

Pregunta 2

Con LiveData, puedes cambiar si quieres que un objeto Button esté habilitado (se puede hacer clic) o no. ¿Cómo te asegurarás de que tu app cambie el botón UpdateNumber para que suceda lo siguiente?

  • Se habilita el botón si myNumber tiene un valor superior a 5.
  • No se habilitará el botón si myNumber es igual o menor que 5.

Supongamos que el diseño que contiene el botón UpdateNumber incluye la variable <data> para el NumbersViewModel, como se muestra a continuación:

<data>
   <variable
       name="NumbersViewModel"
       type="com.example.android.numbersapp.NumbersViewModel" />
</data>

Supongamos que el ID del botón en el archivo de diseño es el siguiente:

android:id="@+id/update_number_button"

¿Qué más debe hacer? Selecciona todas las opciones que correspondan.

  • En la clase NumbersViewModel, define una variable LiveData, myNumber, que represente el número. También define una variable cuyo valor se establezca llamando a Transform.map() en la variable myNumber, que muestra un valor booleano que indica si el número es mayor que 5.

    En específico, en ViewModel, agrega el siguiente código:
val myNumber: LiveData<Int>

val enableUpdateNumberButton = Transformations.map(myNumber) {
   myNumber > 5
}
  • En el diseño XML, configura el atributo android:enabled de update_number_button button en NumberViewModel.enableUpdateNumbersButton.
android:enabled="@{NumbersViewModel.enableUpdateNumberButton}"
  • En el Fragment que usa la clase NumbersViewModel, agrega un observador al atributo enabled del botón.

    En específico, en Fragment, agrega el siguiente código:
// Observer for the enabled attribute
viewModel.enabled.observe(this, Observer<Boolean> { isEnabled ->
   myNumber > 5
})
  • En el archivo de diseño, establece el atributo android:enabled de update_number_button button en "Observable".

Comenzar con la siguiente lección: 7.1 Aspectos básicos de RecyclerView

Para ver vínculos a otros codelabs de este curso, consulta la página de destino de codelabs sobre aspectos básicos de Kotlin para Android.