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 de forma secuencial. Todos los codelabs del curso se enumeran en la página de destino de los codelabs de Android Kotlin Fundamentals.
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 cuándo navegar en el ViewModel
, pero definir las rutas de acceso en los fragmentos y el archivo de navegación. Para lograr este objetivo, usa modelos de vistas, fragmentos, LiveData
y observadores.
El codelab concluye mostrando una forma inteligente de hacer un seguimiento de los estados de los botones con una cantidad mínima de código, de modo que cada botón esté habilitado y se pueda hacer clic en él solo cuando tenga sentido que el usuario lo presione.
Conocimientos que ya deberías tener
Debes estar familiarizado con lo siguiente:
- Compilar una interfaz de usuario (IU) básica con una actividad, fragmentos y vistas
- Navegar entre fragmentos y usar
safeArgs
para pasar datos entre fragmentos - Ver 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 interacciones con 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 hacer un seguimiento de los estados de los botones - Cómo mostrar una barra de notificaciones en respuesta a un evento
Actividades
- Extiende la app de 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 los 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. En la pantalla, se muestran todos los datos de sueño del usuario. El botón Borrar borra de forma permanente todos los datos que la app recopiló del usuario.
La segunda pantalla, que se muestra a la derecha, es para seleccionar una calificación de la calidad del sueño. En la app, la calificación se representa de forma numérica. Para fines de desarrollo, la app muestra los íconos de caras y sus equivalentes numéricos.
El flujo del usuario es el siguiente:
- El usuario abre la app y se muestra la pantalla de monitoreo del sueño.
- El usuario presiona el botón Start. Esto registra la hora de inicio y la muestra. El botón Start está inhabilitado y el botón Stop está habilitado.
- El usuario presiona el botón Detener. Esto registra la hora de finalización y abre la pantalla de calidad del sueño.
- El usuario selecciona un ícono de calidad del sueño. La pantalla se cierra y la pantalla de monitoreo muestra la hora de finalización del sueño y la calidad del sueño. El botón Detener está inhabilitado y el botón Iniciar está habilitado. La app está lista para otra noche.
- El botón Borrar se habilita siempre que haya datos en la base de datos. Cuando el usuario presiona el botón Borrar, se borran todos sus datos sin recurso, no aparece el mensaje "¿Seguro?".
Esta app usa una arquitectura simplificada, como se muestra a continuación en el contexto de la arquitectura completa. La app solo usa 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 con fragmentos y el archivo de navegación. Para ahorrarte trabajo, se proporciona una gran parte de este código.
Paso 1: Inspecciona el código
- Para comenzar, continúa con tu propio código del final del último codelab o descarga el código de partida.
- En el código de inicio, inspecciona
SleepQualityFragment
. Esta clase infla el diseño, obtiene la aplicación y devuelvebinding.root
. - Abre navigation.xml en el editor de diseño. Verás que hay una ruta de navegación de
SleepTrackerFragment
aSleepQualityFragment
y de vuelta deSleepQualityFragment
aSleepTrackerFragment
. - Inspecciona el código de navigation.xml. En particular, busca el
<argument>
llamadosleepNightKey
.
Cuando el usuario pasa deSleepTrackerFragment
aSleepQualityFragment,
, la app pasará unsleepNightKey
aSleepQualityFragment
para la noche que se debe actualizar.
Paso 2: Agrega navegación para el monitoreo de la calidad del sueño
El gráfico de navegación ya incluye las rutas desde SleepTrackerFragment
hasta SleepQualityFragment
y viceversa. Sin embargo, aún no se codificaron los controladores de clics que implementan la navegación de un fragmento al siguiente. Ahora agregarás ese código en ViewModel
.
En el controlador de clics, estableces un 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 terminó, lo que restablece la variable de estado.
- Abre
SleepTrackerViewModel
. Debes agregar navegación para que, cuando el usuario presione el botón Detener, la app navegue aSleepQualityFragment
para recopilar una calificación de calidad. - En
SleepTrackerViewModel
, crea unLiveData
que cambie cuando quieras que la app navegue aSleepQualityFragment
. Usa la encapsulación para exponer solo una versión obtenible deLiveData
aViewModel
.
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
- Agrega una función
doneNavigating()
que restablezca la variable que activa la navegación.
fun doneNavigating() {
_navigateToSleepQuality.value = null
}
- En el controlador de clics del botón Detener,
onStopTracking()
, activa la navegación alSleepQualityFragment
. Establece la variable _navigateToSleepQuality
al final de la función como lo último dentro del bloquelaunch{}
. Ten en cuenta que esta variable se establece ennight
. Cuando esta variable tiene un valor, la app navega aSleepQualityFragment
y pasa el valor de night.
.
_navigateToSleepQuality.value = oldNight
- El
SleepTrackerFragment
debe observar _navigateToSleepQuality
para que la app sepa cuándo navegar. EnSleepTrackerFragment
, enonCreateView()
, agrega un observador paranavigateToSleepQuality()
. Ten en cuenta que la importación para esto es ambigua y debes importarandroidx.lifecycle.Observer
.
sleepTrackerViewModel.navigateToSleepQuality.observe(this, Observer {
})
- Dentro del bloque del observador, navega y pasa el ID de la noche actual, y, luego, llama a
doneNavigating()
. Si tu importación es ambigua, importaandroidx.navigation.fragment.findNavController
.
night ->
night?.let {
this.findNavController().navigate(
SleepTrackerFragmentDirections
.actionSleepTrackerFragmentToSleepQualityFragment(night.nightId))
sleepTrackerViewModel.doneNavigating()
}
- Compila y ejecuta la app. Presiona Start y, luego, Stop, lo 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 volverás al fragmento del monitor de sueño. La pantalla debería actualizarse automáticamente para mostrarle al usuario el valor actualizado. Debes crear un ViewModel
y un ViewModelFactory
, y actualizar el SleepQualityFragment
.
Paso 1: Crea un ViewModel y un ViewModelFactory
- En el paquete
sleepquality
, crea o abre SleepQualityViewModel.kt. - Crea una clase
SleepQualityViewModel
que tome unsleepNightKey
y una base de datos como argumentos. Al igual que conSleepTrackerViewModel
, debes pasardatabase
desde la fábrica. También debes pasar elsleepNightKey
de la navegación.
class SleepQualityViewModel(
private val sleepNightKey: Long = 0L,
val database: SleepDatabaseDao) : ViewModel() {
}
- Dentro de la clase
SleepQualityViewModel
, define unJob
y unuiScope
, y anulaonCleared()
.
private val viewModelJob = Job()
private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
override fun onCleared() {
super.onCleared()
viewModelJob.cancel()
}
- Para volver a
SleepTrackerFragment
con el mismo patrón que antes, declara_navigateToSleepTracker
. ImplementanavigateToSleepTracker
ydoneNavigating()
.
private val _navigateToSleepTracker = MutableLiveData<Boolean?>()
val navigateToSleepTracker: LiveData<Boolean?>
get() = _navigateToSleepTracker
fun doneNavigating() {
_navigateToSleepTracker.value = null
}
- Crea un controlador de clics,
onSetSleepQuality()
, para todas las imágenes de calidad del sueño que se usarán.
Usa el mismo patrón de corrutina que en el codelab anterior:
- Inicia una corrutina en
uiScope
y cambia al despachador de E/S. - Obtén
tonight
consleepNightKey
. - Establece la calidad del sueño.
- Actualiza la base de datos.
- Activa la navegación.
Ten en cuenta que la muestra de código que se incluye a continuación realiza todo el trabajo en el controlador de clics, en lugar de factorizar la operación de la 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
}
}
- En el paquete
sleepquality
, crea o abreSleepQualityViewModelFactory.kt
y agrega la claseSleepQualityViewModelFactory
, como se muestra a continuación. Esta clase usa una versión del mismo código estándar que ya viste. 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
- Abre
SleepQualityFragment.kt
. - En
onCreateView()
, después de obtener elapplication
, debes obtener elarguments
que se proporcionó con la navegación. Estos argumentos se encuentran enSleepQualityFragmentArgs
. Debes extraerlos del paquete.
val arguments = SleepQualityFragmentArgs.fromBundle(arguments!!)
- A continuación, obtén el
dataSource
.
val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao
- Crea una fábrica y pasa el
dataSource
y elsleepNightKey
.
val viewModelFactory = SleepQualityViewModelFactory(arguments.sleepNightKey, dataSource)
- Obtén una referencia de
ViewModel
.
val sleepQualityViewModel =
ViewModelProviders.of(
this, viewModelFactory).get(SleepQualityViewModel::class.java)
- 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
- 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
- Abre el archivo de diseño
fragment_sleep_quality.xml
. En el bloque<data>
, agrega una variable para elSleepQualityViewModel
.
<data>
<variable
name="sleepQualityViewModel"
type="com.example.android.trackmysleepquality.sleepquality.SleepQualityViewModel" />
</data>
- Para cada una de las seis imágenes de calidad del 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)}"
- 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 con corrutinas.
Ahora tu app funciona muy bien. El usuario puede presionar Iniciar y Detener cuantas veces quiera. Cuando el usuario presiona Detener, puede ingresar la calidad del sueño. Cuando el usuario presiona Borrar, todos los datos se borran de forma silenciosa en segundo plano. Sin embargo, todos los botones siempre están habilitados y se puede hacer clic en ellos, lo que no daña la app, pero permite que los usuarios creen noches de sueño incompletas.
En esta última tarea, aprenderás a usar mapas de transformación para administrar la visibilidad de los botones, de modo que los usuarios solo puedan tomar la decisión correcta. Puedes usar un método similar para mostrar un mensaje descriptivo después de que se hayan borrado todos los datos.
Paso 1: Actualiza los estados de los botones
La idea es establecer el estado del botón de modo que, al principio, solo esté habilitado el botón Start, lo que significa que se puede hacer clic en él.
Después de que el usuario presiona Comenzar, se habilita el botón Detener y se inhabilita Comenzar. El botón Borrar solo se habilita cuando hay datos en la base de datos.
- Abre el archivo de diseño
fragment_sleep_tracker.xml
. - Agrega la propiedad
android:enabled
a cada botón. La propiedadandroid:enabled
es un valor booleano que indica si el botón está habilitado o no. (Se puede presionar un botón habilitado, pero no uno inhabilitado). Asigna 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}"
- Abre
SleepTrackerViewModel
y crea tres variables correspondientes. Asigna a cada variable una transformación que la pruebe.
- El botón Start debe estar habilitado cuando
tonight
esnull
. - El botón Detener debe estar habilitado cuando
tonight
no esnull
. - El botón Borrar solo debe habilitarse si
nights
, y, 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()
}
- Ejecuta tu app y experimenta con los botones.
Paso 2: Usa una barra de notificaciones para informar al usuario
Después de que el usuario borre la base de datos, muéstrale una confirmación con el widget Snackbar
. Una Snackbar 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 una interacción del usuario en otro lugar de la pantalla o después de que el usuario la desliza fuera de la pantalla.
Mostrar la barra de mensajes es una tarea de la IU y debe ocurrir en el fragmento. La decisión de mostrar la barra de mensajes se toma en ViewModel
. Para configurar y activar una barra de notificaciones cuando se borran los datos, puedes usar la misma técnica que para activar la navegación.
- En
SleepTrackerViewModel
, crea el evento encapsulado.
private var _showSnackbarEvent = MutableLiveData<Boolean>()
val showSnackBarEvent: LiveData<Boolean>
get() = _showSnackbarEvent
- Luego, implementa
doneShowingSnackbar()
.
fun doneShowingSnackbar() {
_showSnackbarEvent.value = false
}
- En
SleepTrackerFragment
, enonCreateView()
, agrega un observador:
sleepTrackerViewModel.showSnackBarEvent.observe(this, Observer { })
- Dentro del bloque del 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()
}
- En
SleepTrackerViewModel
, activa el evento en el métodoonClear()
. Para ello, establece el valor del evento entrue
dentro del bloquelaunch
:
_showSnackbarEvent.value = true
- 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 musical familiar en una nueva tonalidad. Si bien los detalles cambian, el patrón subyacente de lo que hiciste en los codelabs anteriores de esta lección sigue siendo el mismo. Conocer estos patrones hace que la codificación sea mucho más rápida, ya que puedes reutilizar el código de las apps existentes. Estos son algunos de los patrones que se usaron en este curso hasta ahora:
- Crea un
ViewModel
y unViewModelFactory
, y configura una fuente de datos. - Activa la navegación. Para separar las preocupaciones, coloca el controlador de clics en el ViewModel y la navegación en el fragmento.
- Usa la encapsulación con
LiveData
para hacer un seguimiento de los cambios de estado y responder a ellos. - Usa transformaciones con
LiveData
. - Crea una base de datos singleton.
- Configura corrutinas para las operaciones de la base de datos.
Cómo activar la navegación
En un archivo de navegación, defines las posibles rutas de navegación entre fragmentos. Existen diferentes formas de activar la navegación de un fragmento a otro. Estos incluyen los siguientes:
- Define controladores
onClick
para activar la navegación a un fragmento de destino. - Como alternativa, para habilitar la navegación de un fragmento al siguiente, haz lo siguiente:
- Define un valor de
LiveData
para registrar si debe ocurrir la navegación. - Adjunta un observador a ese valor
LiveData
. - Luego, tu código cambia ese valor cada vez que se debe activar o se completa la navegación.
Cómo establecer el atributo android:enabled
- El atributo
android:enabled
se define enTextView
y lo heredan todas las subclases, incluidaButton
. - El atributo
android:enabled
determina si unView
está habilitado o no. El significado de "habilitado" varía según la subclase. Por ejemplo, unEditText
no habilitado impide que el usuario edite el texto incluido, y unButton
no habilitado impide que el usuario presione el botón. - El atributo
enabled
no es el mismo que el atributovisibility
. - 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:
- Para activar notificaciones al usuario, puedes usar la misma técnica que usas 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 actividades para el hogar para los alumnos que trabajan en este codelab como parte de un curso dirigido por un instructor. Depende del instructor hacer lo siguiente:
- Si es necesario, asigna una tarea.
- Comunicarles a los alumnos cómo enviar las actividades para el hogar.
- Califica las actividades para el hogar.
Los instructores pueden usar estas sugerencias en la medida que quieran y deben asignar cualquier otra actividad para el hogar que consideren apropiada.
Si estás trabajando en este codelab por tu cuenta, usa estas actividades para el hogar para probar tus conocimientos.
Responde estas preguntas:
Pregunta 1
Una forma de habilitar tu app para que active la navegación de un fragmento al siguiente es usar un valor de LiveData
para indicar si se debe activar la navegación o no.
¿Cuáles son los pasos para usar un valor LiveData
, llamado gotoBlueFragment
, para activar la navegación del fragmento rojo al fragmento azul? Seleccione todas las opciones que correspondan:
- En
ViewModel
, define el valorLiveData
comogotoBlueFragment
. - En
RedFragment
, observa el valorgotoBlueFragment
. Implementa el códigoobserve{}
para navegar aBlueFragment
cuando sea apropiado y, luego, restablece el valor degotoBlueFragment
para indicar que se completó la navegación. - 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 ir deRedFragment
aBlueFragment
. - Asegúrate de que tu código defina un controlador
onClick
para elView
en el que el usuario hace clic para navegar aBlueFragment
, donde el controladoronClick
observa el valor degoToBlueFragment
.
Pregunta 2
Puedes cambiar si un Button
está habilitado (se puede hacer clic en él) o no con LiveData
. ¿Cómo te asegurarías de que tu app cambie el botón UpdateNumber
para que suceda lo siguiente?
- El botón se habilita si
myNumber
tiene un valor mayor que 5. - El botón no se habilita si
myNumber
es igual o inferior a 5.
Supón que el diseño que contiene el botón UpdateNumber
incluye la variable <data>
para el NumbersViewModel
, como se muestra aquí:
<data> <variable name="NumbersViewModel" type="com.example.android.numbersapp.NumbersViewModel" /> </data>
Supón que el ID del botón en el archivo de diseño es el siguiente:
android:id="@+id/update_number_button"
¿Qué más debes hacer? Selecciona todas las opciones que correspondan.
- En la clase
NumbersViewModel
, define una variableLiveData
,myNumber
, que represente el número. También define una variable cuyo valor se establece llamando aTransform.map()
en la variablemyNumber
, que devuelve un valor booleano que indica si el número es mayor que 5.
Específicamente, enViewModel
, agrega el siguiente código:
val myNumber: LiveData<Int>
val enableUpdateNumberButton = Transformations.map(myNumber) {
myNumber > 5
}
- En el diseño en XML, establece el atributo
android:enabled
delupdate_number_button button
enNumberViewModel.enableUpdateNumbersButton
.
android:enabled="@{NumbersViewModel.enableUpdateNumberButton}"
- En el
Fragment
que usa la claseNumbersViewModel
, agrega un observador al atributoenabled
del botón.
Específicamente, enFragment
, 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
delupdate_number_button button
en"Observable"
.
Comienza la próxima lección:
Para obtener vínculos a otros codelabs de este curso, consulta la página de destino de los codelabs de Conceptos básicos de Kotlin para Android.