Aspectos básicos de Kotlin para Android 06.2: Corrutinas y Room

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

Una de las principales prioridades para crear una experiencia del usuario impecable en tu app es asegurarte de que la IU siempre responda y se ejecute sin problemas. Una forma de mejorar el rendimiento de la IU es trasladar las tareas de larga duración, como las operaciones de bases de datos, al segundo plano.

En este codelab, implementarás la parte visible para el usuario de la app de TrackMySleepQuality con corrutinas de Kotlin para realizar operaciones de la base de datos fuera del subproceso principal.

Conocimientos que ya deberías tener

Debes estar familiarizado con lo siguiente:

  • Compilar una interfaz de usuario (IU) básica con una actividad, fragmentos, vistas y controladores de clics
  • Navegar entre fragmentos y usar safeArgs para pasar datos simples entre fragmentos
  • Ver modelos, fábricas de modelos de vista, transformaciones y LiveData
  • Cómo crear una base de datos Room, crear un DAO y definir entidades
  • Es útil si conoces los conceptos de subprocesos y multiprocesamiento.

Qué aprenderás

  • Cómo funcionan los subprocesos en Android
  • Cómo usar las corrutinas de Kotlin para alejar las operaciones de la base de datos del subproceso principal
  • Cómo mostrar datos con formato en un TextView

Actividades

  • Extiende la app de TrackMySleepQuality para recopilar, almacenar y mostrar datos en la base de datos y desde ella.
  • Usa corrutinas para ejecutar operaciones de bases de datos de larga duración en segundo plano.
  • Usa LiveData para activar la navegación y la visualización de una barra de notificaciones.
  • Usa LiveData para habilitar e inhabilitar los botones.

En este codelab, compilarás el ViewModel, las corrutinas y las partes de visualización de datos 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 esta tarea, usarás un TextView para mostrar los datos de monitoreo del sueño con formato. (Esta no es la interfaz final. Aprenderás una mejor manera en otro codelab).

Puedes continuar con la app de TrackMySleepQuality que compilaste en el codelab anterior o descargar la app inicial para este codelab.

Paso 1: Descarga y ejecuta la app de inicio

  1. Descarga la app TrackMySleepQuality-Coroutines-Starter de GitHub.
  2. Compila y ejecuta la app. La app muestra la IU del fragmento SleepTrackerFragment, pero no muestra datos. Los botones no responden cuando los presionas.

Paso 2: Inspecciona el código

El código de partida de este codelab es el mismo que el código de solución del codelab 6.1 Crea una base de datos de Room.

  1. Abre res/layout/activity_main.xml. Este diseño contiene el fragmento nav_host_fragment. También observa la etiqueta <merge>.

    La etiqueta merge se puede usar para eliminar diseños redundantes cuando se incluyen diseños, y es una buena idea usarla. Un ejemplo de diseño redundante sería ConstraintLayout > LinearLayout > TextView, en el que el sistema podría eliminar el LinearLayout. Este tipo de optimización puede simplificar la jerarquía de vistas y mejorar el rendimiento de la app.
  2. En la carpeta navigation, abre navigation.xml. Puedes ver dos fragmentos y las acciones de navegación que los conectan.
  3. En la carpeta layout, haz doble clic en el fragmento del monitor de sueño para ver su diseño en XML. Observa lo siguiente:
  • Los datos de diseño se unen en un elemento <layout> para habilitar la vinculación de datos.
  • ConstraintLayout y las otras vistas se organizan dentro del elemento <layout>.
  • El archivo tiene una etiqueta de marcador de posición <data>.

La app de inicio también proporciona dimensiones, colores y diseño para la IU. La app contiene una base de datos Room, un DAO y una entidad SleepNight. Si no completaste el codelab anterior, asegúrate de explorar estos aspectos del código por tu cuenta.

Ahora que tienes una base de datos y una IU, debes recopilar datos, agregarlos a la base de datos y mostrarlos. Todo este trabajo se realiza en el modelo de vista. Tu modelo de vista del monitor de sueño controlará los clics en los botones, interactuará con la base de datos a través del DAO y proporcionará datos a la IU a través de LiveData. Todas las operaciones de la base de datos se deberán ejecutar desde el subproceso de IU principal. Para ello, deberás usar corrutinas.

Paso 1: Agrega SleepTrackerViewModel

  1. En el paquete sleeptracker, abre SleepTrackerViewModel.kt.
  2. Inspecciona la clase SleepTrackerViewModel, que se proporciona en la app de partida y también se muestra a continuación. Ten en cuenta que la clase extiende AndroidViewModel(). Esta clase es igual a ViewModel, pero toma el contexto de la aplicación como un parámetro y lo pone a disposición como una propiedad. La necesitarás más adelante.
class SleepTrackerViewModel(
       val database: SleepDatabaseDao,
       application: Application) : AndroidViewModel(application) {
}

Paso 2: Agrega SleepTrackerViewModelFactory

  1. En el paquete sleeptracker, abre SleepTrackerViewModelFactory.kt.
  2. Examina el código que se te proporciona para la fábrica, que se muestra a continuación:
class SleepTrackerViewModelFactory(
       private val dataSource: SleepDatabaseDao,
       private val application: Application) : ViewModelProvider.Factory {
   @Suppress("unchecked_cast")
   override fun <T : ViewModel?> create(modelClass: Class<T>): T {
       if (modelClass.isAssignableFrom(SleepTrackerViewModel::class.java)) {
           return SleepTrackerViewModel(dataSource, application) as T
       }
       throw IllegalArgumentException("Unknown ViewModel class")
   }
}

Observa lo siguiente:

  • El SleepTrackerViewModelFactory proporcionado toma el mismo argumento que el ViewModel y extiende ViewModelProvider.Factory.
  • Dentro de la fábrica, el código anula create(), que toma cualquier tipo de clase como argumento y devuelve un ViewModel.
  • En el cuerpo de create(), el código verifica que haya una clase SleepTrackerViewModel disponible y, si la hay, devuelve una instancia de ella. De lo contrario, el código arroja una excepción.

Paso 3: Actualiza SleepTrackerFragment

  1. En SleepTrackerFragment, obtén una referencia al contexto de la aplicación. Coloca la referencia en onCreateView(), debajo de binding. Necesitas una referencia a la app a la que se adjunta este fragmento para pasarla al proveedor de la fábrica de modelos de vista.

    La función requireNotNull de Kotlin arroja un IllegalArgumentException si el valor es null.
val application = requireNotNull(this.activity).application
  1. Necesitas una referencia a tu fuente de datos a través de una referencia al DAO. En onCreateView(), antes de return, define un dataSource. Para obtener una referencia al DAO de la base de datos, usa SleepDatabase.getInstance(application).sleepDatabaseDao.
val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao
  1. En onCreateView(), antes del return, crea una instancia de viewModelFactory. Debes pasarle dataSource y application.
val viewModelFactory = SleepTrackerViewModelFactory(dataSource, application)
  1. Ahora que tienes una fábrica, obtén una referencia a SleepTrackerViewModel. El parámetro SleepTrackerViewModel::class.java hace referencia a la clase Java de tiempo de ejecución de este objeto.
val sleepTrackerViewModel =
       ViewModelProviders.of(
               this, viewModelFactory).get(SleepTrackerViewModel::class.java)
  1. El código finalizado debería verse de la siguiente manera:
// Create an instance of the ViewModel Factory.
val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao
val viewModelFactory = SleepTrackerViewModelFactory(dataSource, application)

// Get a reference to the ViewModel associated with this fragment.
val sleepTrackerViewModel =
       ViewModelProviders.of(
               this, viewModelFactory).get(SleepTrackerViewModel::class.java)

Este es el método onCreateView() hasta el momento:

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {

        // Get a reference to the binding object and inflate the fragment views.
        val binding: FragmentSleepTrackerBinding = DataBindingUtil.inflate(
                inflater, R.layout.fragment_sleep_tracker, container, false)

        val application = requireNotNull(this.activity).application

        val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao

        val viewModelFactory = SleepTrackerViewModelFactory(dataSource, application)

        val sleepTrackerViewModel =
                ViewModelProviders.of(
                        this, viewModelFactory).get(SleepTrackerViewModel::class.java)

        return binding.root
    }

Paso 4: Agrega la vinculación de datos para el modelo de vista

Con el ViewModel básico en su lugar, debes terminar de configurar la vinculación de datos en el SleepTrackerFragment para conectar el ViewModel con la IU.


En el archivo de diseño fragment_sleep_tracker.xml, haz lo siguiente:

  1. Dentro del bloque <data>, crea un <variable> que haga referencia a la clase SleepTrackerViewModel.
<data>
   <variable
       name="sleepTrackerViewModel"
       type="com.example.android.trackmysleepquality.sleeptracker.SleepTrackerViewModel" />
</data>

En SleepTrackerFragment:

  1. Establece la actividad actual como el propietario del ciclo de vida de la vinculación. Agrega este código dentro del método onCreateView(), antes de la instrucción return:
binding.setLifecycleOwner(this)
  1. Asigna la variable de vinculación sleepTrackerViewModel a sleepTrackerViewModel. Coloca este código dentro de onCreateView(), debajo del código que crea el SleepTrackerViewModel:
binding.sleepTrackerViewModel = sleepTrackerViewModel
  1. Es probable que veas un error porque debes volver a crear el objeto de vinculación. Limpia y vuelve a compilar el proyecto para deshacerte del error.
  2. Por último, como siempre, asegúrate de que tu código se compile y ejecute sin errores.

En Kotlin, las corrutinas son la forma de controlar tareas de larga duración de forma elegante y eficiente. Las corrutinas de Kotlin te permiten convertir en código secuencial el código basado en devoluciones de llamada. Por lo general, el código escrito de forma secuencial es más fácil de leer y hasta puede usar funciones de lenguaje como excepciones. En definitiva, las corrutinas y las devoluciones de llamada hacen lo mismo: esperan hasta que un resultado esté disponible desde una tarea de larga duración y continúan su ejecución.

Las corrutinas tienen las siguientes propiedades:

  • Las corrutinas son asíncronas y no provocan bloqueos.
  • Las corrutinas usan funciones de suspensión para hacer que el código asíncrono sea secuencial.

Las corrutinas son asíncronas.

Una corrutina se ejecuta de forma independiente de los pasos de ejecución principales de tu programa. Esto podría hacerse en paralelo o en un procesador independiente. También podría ser que, mientras el resto de la app espera la entrada, tú realices un poco de procesamiento. Uno de los aspectos importantes de async es que no puedes esperar que el resultado esté disponible hasta que esperes explícitamente a que lo esté.

Por ejemplo, supongamos que tienes una pregunta que requiere investigación y le pides a un compañero que encuentre la respuesta. Se van y trabajan en ello, lo que es como si estuvieran trabajando "de forma asíncrona" y "en un subproceso independiente". Puedes seguir haciendo otro trabajo que no dependa de la respuesta hasta que tu compañero regrese y te diga cuál es.

Las corrutinas no provocan bloqueos.

No bloqueador significa que una corrutina no bloquea el subproceso principal ni el de IU. Por lo tanto, con las corrutinas, los usuarios siempre tienen la experiencia más fluida posible, ya que la interacción de la IU siempre tiene prioridad.

Las corrutinas usan funciones de suspensión para hacer que el código asíncrono sea secuencial.

La palabra clave suspend es la forma en que Kotlin marca una función, o un tipo de función, como disponible para las corrutinas. Cuando una corrutina llama a una función marcada con suspend, en lugar de bloquearse hasta que se muestre la función como una llamada a función normal, la corrutina suspende la ejecución hasta que el resultado esté listo. Luego, la corrutina se reanuda donde se detuvo, con el resultado.

Mientras la corrutina está suspendida y espera un resultado, desbloquea el subproceso en el que se está ejecutando. De esa manera, se pueden ejecutar otras funciones o corrutinas.

La palabra clave suspend no especifica el subproceso en el que se ejecuta el código. Una función de suspensión puede ejecutarse en un subproceso en segundo plano o en el subproceso principal.

Para usar corrutinas en Kotlin, necesitas tres elementos:

  • Un trabajo
  • Un despachador
  • Un permiso

Trabajo: Básicamente, un trabajo es cualquier cosa que se puede cancelar. Cada corrutina tiene un trabajo, y puedes usar el trabajo para cancelar la corrutina. Los trabajos se pueden organizar en jerarquías de elementos superiores y secundarios. Si cancelas un trabajo principal, se cancelarán de inmediato todos los trabajos secundarios, lo que es mucho más conveniente que cancelar cada corrutina de forma manual.

Despachador: El despachador envía corrutinas para que se ejecuten en varios subprocesos. Por ejemplo, Dispatcher.Main ejecuta tareas en el subproceso principal y Dispatcher.IO descarga tareas de bloqueo de E/S en un grupo compartido de subprocesos.

Alcance: El alcance de una corrutina define el contexto en el que se ejecuta la corrutina. Un alcance combina información sobre el trabajo y el despachador de una corrutina. Los alcances realizan un seguimiento de las corrutinas. Cuando inicias una corrutina, esta se encuentra "en un alcance", lo que significa que indicaste qué alcance hará un seguimiento de la corrutina.

Deseas que el usuario pueda interactuar con los datos de sueño de las siguientes maneras:

  • Cuando el usuario presiona el botón Start, la app crea una nueva noche de sueño y la almacena en la base de datos.
  • Cuando el usuario presiona el botón Detener, la app actualiza la noche con una hora de finalización.
  • Cuando el usuario presiona el botón Borrar, la app borra los datos de la base de datos.

Estas operaciones de la base de datos pueden demorar mucho tiempo, por lo que deben ejecutarse en un subproceso independiente.

Paso 1: Configura corrutinas para las operaciones de la base de datos

Cuando se presiona el botón Start en la app de Sleep Tracker, debes llamar a una función en SleepTrackerViewModel para crear una instancia nueva de SleepNight y almacenarla en la base de datos.

Si presionas cualquiera de los botones, se activa una operación de base de datos, como crear o actualizar un SleepNight. Por este motivo y otros, usas corrutinas para implementar controladores de clics para los botones de la app.

  1. Abre el archivo build.gradle a nivel de la app y busca las dependencias de las corrutinas. Para usar corrutinas, necesitas estas dependencias, que ya se agregaron por ti.

    El $coroutine_version se define en el archivo build.gradle del proyecto como coroutine_version = '1.0.0'.
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutine_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutine_version"
  1. Abre el archivo SleepTrackerViewModel.
  2. En el cuerpo de la clase, define viewModelJob y asígnale una instancia de Job. Este viewModelJob te permite cancelar todas las corrutinas que inició este modelo de vista cuando ya no se usa y se destruye. De esta manera, no terminarás con corrutinas que no tienen a dónde regresar.
private var viewModelJob = Job()
  1. Al final del cuerpo de la clase, anula onCleared() y cancela todas las corrutinas. Cuando se destruye el ViewModel, se llama a onCleared().
override fun onCleared() {
   super.onCleared()
   viewModelJob.cancel()
}
  1. Justo debajo de la definición de viewModelJob, define un uiScope para las corrutinas. El alcance determina en qué subproceso se ejecutará la corrutina, y el alcance también debe conocer el trabajo. Para obtener un alcance, solicita una instancia de CoroutineScope y pasa un dispatcher y un trabajo.

Usar Dispatchers.Main significa que las corrutinas iniciadas en uiScope se ejecutarán en el subproceso principal. Esto tiene sentido para muchas corrutinas iniciadas por un ViewModel, ya que, después de que estas corrutinas realizan algún procesamiento, generan una actualización de la IU.

private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
  1. Debajo de la definición de uiScope, define una variable llamada tonight para contener la noche actual. Haz que la variable sea MutableLiveData, ya que necesitas poder observar los datos y cambiarlos.
private var tonight = MutableLiveData<SleepNight?>()
  1. Para inicializar la variable tonight lo antes posible, crea un bloque init debajo de la definición de tonight y llama a initializeTonight(). Definirás initializeTonight() en el siguiente paso.
init {
   initializeTonight()
}
  1. Debajo del bloque init, implementa initializeTonight(). En uiScope, inicia una corrutina. En el interior, obtén el valor de tonight de la base de datos llamando a getTonightFromDatabase() y asigna el valor a tonight.value. Definirás getTonightFromDatabase() en el siguiente paso.
private fun initializeTonight() {
   uiScope.launch {
       tonight.value = getTonightFromDatabase()
   }
}
  1. Implementar getTonightFromDatabase() Defínela como una función private suspend que muestre un SleepNight anulable si no hay un SleepNight iniciado actualmente. Esto te dejará con un error, ya que la función tiene que devolver algo.
private suspend fun getTonightFromDatabase(): SleepNight? { }
  1. Dentro del cuerpo de la función getTonightFromDatabase(), devuelve el resultado de una corrutina que se ejecuta en el contexto de Dispatchers.IO. Usa el despachador de E/S, ya que obtener datos de la base de datos es una operación de E/S y no tiene nada que ver con la IU.
  return withContext(Dispatchers.IO) {}
  1. Dentro del bloque de devolución, permite que la corrutina obtenga la noche (la más reciente) de la base de datos. Si las horas de inicio y finalización no son las mismas, lo que significa que la noche ya se completó, devuelve null. De lo contrario, devuelve la noche.
       var night = database.getTonight()
       if (night?.endTimeMilli != night?.startTimeMilli) {
           night = null
       }
       night

La función de suspensión getTonightFromDatabase() completada debería verse de la siguiente manera: No debería haber más errores.

private suspend fun getTonightFromDatabase(): SleepNight? {
   return withContext(Dispatchers.IO) {
       var night = database.getTonight()
       if (night?.endTimeMilli != night?.startTimeMilli) {
           night = null
       }
       night
   }
}

Paso 2: Agrega el controlador de clics para el botón Iniciar

Ahora puedes implementar onStartTracking(), el controlador de clics para el botón Start. Debes crear un nuevo SleepNight, insertarlo en la base de datos y asignarlo a tonight. La estructura de onStartTracking() se parecerá mucho a la de initializeTonight().

  1. Comienza con la definición de la función para onStartTracking(). Puedes colocar los controladores de clics sobre onCleared() en el archivo onCleared().SleepTrackerViewModel
fun onStartTracking() {}
  1. Dentro de onStartTracking(), inicia una corrutina en uiScope, ya que necesitas este resultado para continuar y actualizar la IU.
uiScope.launch {}
  1. Dentro del inicio de la corrutina, crea un nuevo SleepNight, que captura la hora actual como la hora de inicio.
        val newNight = SleepNight()
  1. Aún dentro del inicio de la corrutina, llama a insert() para insertar newNight en la base de datos. Verás un error porque aún no definiste esta función de suspensión insert(). (Esta no es la función de DAO con el mismo nombre).
       insert(newNight)
  1. También dentro del inicio de la corrutina, actualiza tonight.
       tonight.value = getTonightFromDatabase()
  1. Debajo de onStartTracking(), define insert() como una función private suspend que toma un SleepNight como argumento.
private suspend fun insert(night: SleepNight) {}
  1. Para el cuerpo de insert(), inicia una corrutina en el contexto de E/S y llama a insert() desde el DAO para insertar la noche en la base de datos.
   withContext(Dispatchers.IO) {
       database.insert(night)
   }
  1. En el archivo de diseño fragment_sleep_tracker.xml, agrega el controlador de clics para onStartTracking() al start_button con la magia de la vinculación de datos que configuraste anteriormente. La notación de la función @{() -> crea una función lambda que no toma argumentos y llama al controlador de clics en sleepTrackerViewModel.
android:onClick="@{() -> sleepTrackerViewModel.onStartTracking()}"
  1. Compila y ejecuta tu app. Presiona el botón Start. Esta acción crea datos, pero aún no puedes ver nada. Eso lo solucionarás más tarde.
fun someWorkNeedsToBeDone {
   uiScope.launch {

        suspendFunction()

   }
}

suspend fun suspendFunction() {
   withContext(Dispatchers.IO) {
       longrunningWork()
   }
}

Paso 3: Muestra los datos

En SleepTrackerViewModel, la variable nights hace referencia a LiveData porque getAllNights() en el DAO devuelve LiveData.

Es una función de Room que, cada vez que cambian los datos de la base de datos, se actualiza el LiveData nights para mostrar los datos más recientes. Nunca es necesario que configures LiveData de forma explícita ni que lo actualices. Room actualiza los datos para que coincidan con la base de datos.

Sin embargo, si muestras nights en una vista de texto, se mostrará la referencia del objeto. Para ver el contenido del objeto, transforma los datos en una cadena con formato. Usa un mapa de Transformation que se ejecute cada vez que nights reciba datos nuevos de la base de datos.

  1. Abre el archivo Util.kt y quita el comentario del código para la definición de formatNights() y las instrucciones import asociadas. Para quitar las marcas de comentario del código en Android Studio, selecciona todo el código marcado con // y presiona Cmd+/ o Control+/.
  2. Ten en cuenta que formatNights() devuelve un tipo Spanned, que es una cadena con formato HTML.
  3. Abre strings.xml. Observa el uso de CDATA para dar formato a los recursos de cadena y mostrar los datos de sueño.
  4. Abre SleepTrackerViewModel. En la clase SleepTrackerViewModel, debajo de la definición de uiScope, define una variable llamada nights. Obtén todas las noches de la base de datos y asígnalas a la variable nights.
private val nights = database.getAllNights()
  1. Justo debajo de la definición de nights, agrega código para transformar nights en un nightsString. Usa la función formatNights() de Util.kt.

    Pasa nights a la función map() de la clase Transformations. Para acceder a tus recursos de cadena, define la función de asignación como una llamada a formatNights(). Proporciona nights y un objeto Resources.
val nightsString = Transformations.map(nights) { nights ->
   formatNights(nights, application.resources)
}
  1. Abre el archivo de diseño fragment_sleep_tracker.xml. En TextView, en la propiedad android:text, ahora puedes reemplazar la cadena de recursos por una referencia a nightsString.
"@{sleepTrackerViewModel.nightsString}"
  1. Vuelve a compilar el código y ejecuta la app. Ahora deberían mostrarse todos tus datos de sueño con las horas de inicio.
  2. Presiona el botón Comenzar algunas veces más y verás más datos.

En el siguiente paso, habilitarás la funcionalidad del botón Detener.

Paso 4: Agrega el controlador de clics para el botón Detener

Con el mismo patrón que en el paso anterior, implementa el controlador de clics para el botón Detener en SleepTrackerViewModel..

  1. Agrega onStopTracking() al ViewModel. Inicia una corrutina en uiScope. Si aún no se configuró la hora de finalización, establece endTimeMilli en la hora actual del sistema y llama a update() con los datos de la noche.

    En Kotlin, la sintaxis return@label especifica la función desde la que se muestra esta instrucción, entre varias funciones anidadas.
fun onStopTracking() {
   uiScope.launch {
       val oldNight = tonight.value ?: return@launch
       oldNight.endTimeMilli = System.currentTimeMillis()
       update(oldNight)
   }
}
  1. Implementa update() con el mismo patrón que usaste para implementar insert().
private suspend fun update(night: SleepNight) {
   withContext(Dispatchers.IO) {
       database.update(night)
   }
}
  1. Para conectar el controlador de clics a la IU, abre el archivo de diseño fragment_sleep_tracker.xml y agrega el controlador de clics a stop_button.
android:onClick="@{() -> sleepTrackerViewModel.onStopTracking()}"
  1. Compila y ejecuta tu app.
  2. Presiona Iniciar y, luego, Detener. Verás la hora de inicio, la hora de finalización, la calidad del sueño sin valor y el tiempo que dormiste.

Paso 5: Agrega el controlador de clics para el botón Borrar

  1. De manera similar, implementa onClear() y clear().
fun onClear() {
   uiScope.launch {
       clear()
       tonight.value = null
   }
}

suspend fun clear() {
   withContext(Dispatchers.IO) {
       database.clear()
   }
}
  1. Para conectar el controlador de clics a la IU, abre fragment_sleep_tracker.xml y agrega el controlador de clics a clear_button.
android:onClick="@{() -> sleepTrackerViewModel.onClear()}"
  1. Compila y ejecuta tu app.
  2. Presiona Borrar para deshacerte de todos los datos. Luego, presiona Iniciar y Detener para crear datos nuevos.

Proyecto de Android Studio: TrackMySleepQualityCoroutines

  • Usa ViewModel, ViewModelFactory y la vinculación de datos para configurar la arquitectura de la IU de la app.
  • Para que la IU se siga ejecutando de manera fluida, usa corrutinas en las tareas de larga duración, como todas las operaciones de bases de datos.
  • Las corrutinas son asíncronas y no provocan bloqueos. Usan funciones suspend para hacer que el código asíncrono sea secuencial.
  • Cuando una corrutina llama a una función marcada con suspend, en lugar de bloquearse hasta que se muestre esa función como una llamada a función normal, suspende la ejecución hasta que el resultado esté listo. Luego, se reanuda donde se interrumpió con el resultado.
  • La diferencia entre bloquear y suspender es que, si se bloquea un subproceso, no se realiza ningún otro trabajo. Si el subproceso se suspende, se realiza otro trabajo hasta que el resultado esté disponible.

Para iniciar una corrutina, necesitas un trabajo, un dispatcher y un alcance:

  • Básicamente, un trabajo es cualquier cosa que se puede cancelar. Cada corrutina tiene un trabajo, y puedes usar un trabajo para cancelar una corrutina.
  • El despachador envía corrutinas para que se ejecuten en varios subprocesos. Dispatcher.Main ejecuta tareas en el subproceso principal, y Dispartcher.IO se usa para descargar tareas de bloqueo de E/S en un grupo compartido de subprocesos.
  • El alcance combina información, incluido un trabajo y un despachador, para definir el contexto en el que se ejecuta la corrutina. Los alcances realizan un seguimiento de las corrutinas.

Para implementar controladores de clics que activen operaciones de bases de datos, sigue este patrón:

  1. Inicia una corrutina que se ejecuta en el subproceso principal o de IU, ya que el resultado afecta la IU.
  2. Llama a una función de suspensión para realizar el trabajo de larga duración, de modo que no bloquees el subproceso de IU mientras esperas el resultado.
  3. El trabajo de larga duración no tiene nada que ver con la IU, por lo que debes cambiar al contexto de E/S. De esa manera, el trabajo se puede ejecutar en un grupo de subprocesos optimizado y reservado para este tipo de operaciones.
  4. Luego, llama a la función de la base de datos para que realice el trabajo.

Usa un mapa Transformations para crear una cadena a partir de un objeto LiveData cada vez que el objeto cambie.

Curso de Udacity:

Documentación para desarrolladores de Android:

Otros artículos y documentación:

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

¿Cuáles de las siguientes son ventajas de las corrutinas?

  • No bloquean
  • Se ejecutan de forma asíncrona.
  • Se pueden ejecutar en un subproceso diferente del principal.
  • Siempre hacen que las apps se ejecuten más rápido.
  • Pueden usar excepciones.
  • Se pueden escribir y leer como código lineal.

Pregunta 2

¿Qué es una función de suspensión?

  • Una función común anotada con la palabra clave suspend.
  • Una función que puede llamarse dentro de corrutinas.
  • Mientras se ejecuta una función de suspensión, se suspende el subproceso de llamada.
  • Las funciones de suspensión siempre deben ejecutarse en segundo plano.

Pregunta 3

¿Cuál es la diferencia entre bloquear y suspender un hilo? Marca todas las opciones verdaderas.

  • Cuando se bloquea la ejecución, no se puede ejecutar ningún otro trabajo en el subproceso bloqueado.
  • Cuando se suspende la ejecución, el subproceso puede realizar otros trabajos mientras se completa el trabajo descargado.
  • Suspender es más eficiente, porque los subproceso no pueden quedar esperando, sin hacer nada.
  • Ya sea que esté bloqueada o suspendida, la ejecución aún espera el resultado de la corrutina antes de continuar.

Ir a la siguiente lección: 6.3 Cómo usar LiveData para controlar los estados de los botones

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.