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
- Para comenzar, continúa con tu propio código desde el final del último codelab o descarga el código de inicio.
- En tu código de inicio, inspecciona
SleepQualityFragment
. Esta clase aumenta el diseño, obtiene la aplicación y muestrabinding.root
. - Abre navigation.xml en el editor de diseño. Verás que hay una ruta de navegación de
SleepTrackerFragment
aSleepQualityFragment
y de regreso aSleepQualityFragment
.SleepTrackerFragment
- Inspecciona el código de navigation.xml. En particular, busca el elemento
<argument>
con el nombresleepNightKey
.
Cuando el usuario pase deSleepTrackerFragment
aSleepQualityFragment,
, la app pasará unsleepNightKey
aSleepQualityFragment
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.
- Abre
SleepTrackerViewModel
. Debes agregar una 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 el encapsulamiento para exponer solo una versión deLiveData
que se pueda obtener enViewModel
.
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 Stop,
onStopTracking()
, activa la navegación haciaSleepQualityFragment
. Configura la variable _navigateToSleepQuality
al final de la función como último elemento dentro del bloquelaunch{}
. Ten en cuenta que esta variable está configurada ennight
. Cuando esta variable tiene un valor, la app navega aSleepQualityFragment
y pasa la noche.
_navigateToSleepQuality.value = oldNight
SleepTrackerFragment
necesita observar _navigateToSleepQuality
para que la app sepa cuándo navegar. En elSleepTrackerFragment
, enonCreateView()
, agrega un observador paranavigateToSleepQuality()
. Ten en cuenta que la importación es ambigua y deberás importarandroidx.lifecycle.Observer
.
sleepTrackerViewModel.navigateToSleepQuality.observe(this, Observer {
})
- 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, importaandroidx.navigation.fragment.findNavController
.
night ->
night?.let {
this.findNavController().navigate(
SleepTrackerFragmentDirections
.actionSleepTrackerFragmentToSleepQualityFragment(night.nightId))
sleepTrackerViewModel.doneNavigating()
}
- 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
- 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 con elSleepTrackerViewModel
, debes pasar eldatabase
de la fábrica. También debes pasar elsleepNightKey
desde 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 se indicó anteriormente, 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 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
mediantesleepNightKey
. - 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
}
}
- 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 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
- Abre
SleepQualityFragment.kt
. - En
onCreateView()
, después de obtener laapplication
, debes obtener laarguments
que vino con la navegación. Estos argumentos se encuentran enSleepQualityFragmentArgs
. Debes extraerlas del paquete.
val arguments = SleepQualityFragmentArgs.fromBundle(arguments!!)
- A continuación, obtén la
dataSource
.
val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao
- Crea una fábrica y pasa los objetos
dataSource
ysleepNightKey
.
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 paraSleepQualityViewModel
.
<data>
<variable
name="sleepQualityViewModel"
type="com.example.android.trackmysleepquality.sleepquality.SleepQualityViewModel" />
</data>
- 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)}"
- 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.
- 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; 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}"
- 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 seanull
. - 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()
}
- 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.
- 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 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()
}
- 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 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 unViewModelFactory
, 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 enTextView
y lo heredan todas las subclases, incluidoButton
. - El atributo
android:enabled
determina si se habilita o no unView
. El significado de "habilitado" varía según la subclase. Por ejemplo, un elementoEditText
no habilitado evita que el usuario edite el texto contenido, y un elementoButton
no habilitado impide que presione el botón. - El atributo
enabled
no es lo 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:
- 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 valorLiveData
gotoBlueFragment
. - En
RedFragment
, observa el valorgotoBlueFragment
. Implementa el códigoobserve{}
para navegar aBlueFragment
cuando corresponda y, luego, restablece el valor degotoBlueFragment
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 deRedFragment
aBlueFragment
. - Asegúrate de que tu código defina un controlador
onClick
para elView
en el que el usuario hace clic a fin de navegar aBlueFragment
, donde el controladoronClick
observa el valorgoToBlueFragment
.
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 variableLiveData
,myNumber
, que represente el número. También define una variable cuyo valor se establezca llamando aTransform.map()
en la variablemyNumber
, que muestra un valor booleano que indica si el número es mayor que 5.
En específico, enViewModel
, 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
deupdate_number_button button
enNumberViewModel.enableUpdateNumbersButton
.
android:enabled="@{NumbersViewModel.enableUpdateNumberButton}"
- En el
Fragment
que usa la claseNumbersViewModel
, agrega un observador al atributoenabled
del botón.
En específico, 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
deupdate_number_button button
en"Observable"
.
Comenzar con la siguiente lección:
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.