Cómo usar corrutinas de Kotlin en tu app para Android

En este codelab, aprenderás a usar las corrutinas de Kotlin en una app para Android, una nueva forma de administrar subprocesos en segundo plano que pueden simplificar el código reduciendo la necesidad de devoluciones de llamada. Las corrutinas son una función de Kotlin que convierte devoluciones de llamada asíncronas para tareas de larga duración, como el acceso a la base de datos o a la red, en código secuencial.

A continuación, te mostramos un fragmento de código que te dará una idea de lo que harás.

// Async callbacks
networkRequest { result ->
   // Successful network request
   databaseSave(result) { rows ->
     // Result saved
   }
}

El código basado en devoluciones de llamada se convertirá en código secuencial mediante corrutinas.

// The same code with coroutines
val result = networkRequest()
// Successful network request
databaseSave(result)
// Result saved

Comenzarás con una app existente, compilada con los componentes de la arquitectura, que usa un estilo de devolución de llamada para tareas de larga duración.

Al final de este codelab, tendrás suficiente experiencia para usar corrutinas en tu app para cargar datos de la red y podrás integrar corrutinas en una app. También conocerás las prácticas recomendadas para las corrutinas y cómo escribir una prueba en el código que use corrutinas.

Requisitos previos

  • Conocer los componentes de la arquitectura ViewModel, LiveData, Repository y Room
  • Experiencia con la sintaxis de Kotlin, incluidas las funciones de extensión y lambdas
  • Conocimientos básicos sobre el uso de subprocesos en Android, como el subproceso principal, los subprocesos en segundo plano y las devoluciones de llamada

Actividades

  • Llamar a código escrito con corrutinas y obtener resultados
  • Usa funciones de suspensión para hacer que el código asíncrono sea secuencial.
  • Usa launch y runBlocking para controlar cómo se ejecuta el código.
  • Aprende técnicas para convertir API existentes en corrutinas con suspendCoroutine.
  • Cómo usar corrutinas con componentes de arquitectura
  • Conoce las prácticas recomendadas para probar corrutinas.

Requisitos

  • Android Studio 3.5 (el codelab puede funcionar con otras versiones, pero es posible que falten algunas cosas o se vean diferentes).

Si a medida que avanzas con este codelab encuentras algún problema (errores de código, errores gramaticales, texto poco claro, etc.), infórmalo mediante el vínculo Informa un error que se encuentra en la esquina inferior izquierda del codelab.

Descarga el código

Haz clic en el siguiente vínculo a fin de descargar todo el código de este codelab:

Download Zip

… o clona el repositorio de GitHub desde la línea de comandos con el siguiente comando:

$ git clone https://github.com/googlecodelabs/kotlin-coroutines.git

Preguntas frecuentes

Primero, veamos el aspecto de la app de ejemplo inicial. Sigue estas instrucciones para abrir la app de muestra en Android Studio.

  1. Si descargaste el archivo ZIP kotlin-coroutines, descomprímelo.
  2. Abre el proyecto coroutines-codelab en Android Studio.
  3. Selecciona el módulo de aplicación start.
  4. Haz clic en el botón execute.pngRun y elige un emulador o conecta tu dispositivo Android, que debe poder ejecutar Android Lollipop (el SDK mínimo compatible es el 21). Debería aparecer la pantalla Corrutinas de Kotlin:

Esta app inicial usa subprocesos para aumentar el recuento de una demora breve después de presionar la pantalla. También recuperará un nuevo título de la red y lo mostrará en pantalla. Pruébalo ahora. Deberías ver el recuento y el cambio en el mensaje después de una breve demora. En este codelab, convertirás esta aplicación para que use corrutinas.

Esta app usa los componentes de la arquitectura para separar el código de IU en MainActivity de la lógica de la aplicación en MainViewModel. Tómate un momento para familiarizarte con la estructura del proyecto.

  1. MainActivity muestra la IU, registra los objetos de escucha de clics y puede mostrar una Snackbar. Pasa eventos a MainViewModel y actualiza la pantalla en función de LiveData en MainViewModel.
  2. MainViewModel controla los eventos de onMainViewClicked y se comunica con MainActivity mediante LiveData..
  3. Executors define BACKGROUND,, que puede ejecutar elementos en un subproceso en segundo plano.
  4. TitleRepository recupera los resultados de la red y los guarda en la base de datos.

Cómo agregar corrutinas a un proyecto

Para usar corrutinas en Kotlin, debes incluir la biblioteca coroutines-core en el archivo build.gradle (Module: app) de tu proyecto. Los proyectos de codelab ya lo hicieron por ti, así que no necesitas hacerlo para completar el codelab.

Las corrutinas en Android están disponibles como biblioteca principal y en extensiones específicas de Android:

  • kotlinx-corountines-core : Interfaz principal para usar corrutinas en Kotlin
  • kotlinx-coroutines-android: Compatibilidad con el subproceso principal de Android en corrutinas

La app de inicio ya incluye las dependencias en build.gradle.Cuando creas un nuevo proyecto de app, deberás abrir build.gradle (Module: app) y agregar las dependencias de corrutinas al proyecto.

dependencies {
  ...
  implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:x.x.x"
  implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:x.x.x"
}

En Android, es fundamental evitar el bloqueo del subproceso principal. El subproceso principal es un subproceso único que administra todas las actualizaciones de la IU. También es el subproceso que llama a todos los controladores de clics y otras devoluciones de llamada de la IU. Por lo tanto, debe ejecutarse sin problemas para garantizar una excelente experiencia del usuario.

Para que tu app se muestre al usuario sin pausas visibles, el subproceso principal debe actualizar la pantalla cada 16 ms o más, lo que equivale a unos 60 fotogramas por segundo. Muchas tareas comunes tardan más que este proceso, como analizar grandes conjuntos de datos JSON, escribir datos en una base de datos o recuperar datos desde la red. Por lo tanto, llamar a código como este desde el subproceso principal puede hacer que la app se detenga, salte o incluso se bloquee. Si bloqueas el subproceso principal durante demasiado tiempo, la app incluso puede fallar y mostrar un diálogo Aplicación no responde.

Mira el siguiente video para obtener una introducción sobre cómo las corrutinas resuelven este problema en Android introduciendo la seguridad del subproceso principal.

El patrón de devolución de llamada

Un patrón para realizar tareas de larga duración sin bloquear el subproceso principal es la devolución de llamada. Si usas devoluciones de llamada, podrás iniciar tareas de larga duración en un subproceso que se ejecute en segundo plano. Cuando se completa la tarea, se llama a la devolución de llamada para informarte sobre el resultado del subproceso principal.

Observa un ejemplo del patrón de devolución de llamada.

// Slow request with callbacks
@UiThread
fun makeNetworkRequest() {
    // The slow network request runs on another thread
    slowFetch { result ->
        // When the result is ready, this callback will get the result
        show(result)
    }
    // makeNetworkRequest() exits after calling slowFetch without waiting for the result
}

Debido a que este código está anotado con @UiThread, debe ejecutarse lo suficientemente rápido como para ejecutarse en el subproceso principal. Esto significa que debe regresar muy rápido, de modo que la próxima actualización de pantalla no se retrase. Sin embargo, como slowFetch tardará segundos o incluso minutos en completarse, la conversación principal no puede esperar el resultado. La devolución de llamada show(result) permite que slowFetch se ejecute en un subproceso en segundo plano y muestre el resultado cuando esté listo.

Cómo usar corrutinas para quitar devoluciones de llamada

Las devoluciones de llamada son un patrón muy útil. Sin embargo, tienen algunas desventajas. El código que utiliza devoluciones de llamadas en gran medida puede ser difícil de leer y razonar. Además, las devoluciones de llamada no permiten el uso de algunas funciones del lenguaje, como excepciones.

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.

Al final, hacen lo mismo: esperan hasta que un resultado esté disponible desde una tarea de larga duración y continúan su ejecución. Sin embargo, el código se ve muy diferente.

La palabra clave suspend es la forma en que Kotlin marca una función (o tipo de función) que está disponible para las corrutinas. Cuando una corrutina llama a una función marcada como suspend, en lugar de bloquearse hasta que se muestre una función como una normal, suspende la ejecución hasta que el resultado esté listo y, luego, se reanuda desde donde se detuvo. Mientras está suspendido a la espera de un resultado, desbloquea el subproceso en el que se ejecuta para que se puedan ejecutar otras funciones o corrutinas.

Por ejemplo, en el siguiente código, makeNetworkRequest() y slowFetch() son funciones suspend.

// Slow request with coroutines
@UiThread
suspend fun makeNetworkRequest() {
    // slowFetch is another suspend function so instead of 
    // blocking the main thread  makeNetworkRequest will `suspend` until the result is 
    // ready
    val result = slowFetch()
    // continue to execute after the result is ready
    show(result)
}

// slowFetch is main-safe using coroutines
suspend fun slowFetch(): SlowResult { ... }

Al igual que con la versión de devolución de llamada, makeNetworkRequest debe regresar del subproceso principal de inmediato porque está marcada como @UiThread. Eso significa que, por lo general, no puede llamar a métodos de bloqueo como slowFetch. Aquí es donde la palabra clave suspend hace su magia.

En comparación con el código basado en devoluciones de llamada, el código de corrutinas logra el mismo resultado de desbloquear el subproceso actual con menos código. Debido a su estilo secuencial, es fácil encadenar varias tareas de larga duración sin crear varias devoluciones de llamada. Por ejemplo, el código que obtiene un resultado de dos extremos de red y lo guarda en la base de datos puede escribirse como una función en corrutinas sin devoluciones de llamada. Así es:

// Request data from network and save it to database with coroutines

// Because of the @WorkerThread, this function cannot be called on the
// main thread without causing an error.
@WorkerThread
suspend fun makeNetworkRequest() {
    // slowFetch and anotherFetch are suspend functions
    val slow = slowFetch()
    val another = anotherFetch()
    // save is a regular function and will block this thread
    database.save(slow, another)
}

// slowFetch is main-safe using coroutines
suspend fun slowFetch(): SlowResult { ... }
// anotherFetch is main-safe using coroutines
suspend fun anotherFetch(): AnotherResult { ... }

En la próxima sección, presentarás corrutinas en la app de muestra.

En este ejercicio, escribirás una corrutina para mostrar un mensaje después de una demora. Para comenzar, asegúrate de tener el módulo start abierto en Android Studio.

Información sobre CoroutineScope

En Kotlin, todas las corrutinas se ejecutan dentro de un CoroutineScope. Un alcance controla las corrutinas desde el principio con su trabajo. Cuando cancelas el trabajo de un alcance, se cancelan todas las corrutinas que se iniciaron en ese alcance. En Android, puedes usar un alcance para cancelar todas las corrutinas en ejecución cuando, por ejemplo, el usuario sale de un elemento Activity o Fragment. Los alcances también le permiten especificar un despachador predeterminado. Un despachador controla qué subproceso ejecuta una corrutina.

En el caso de las corrutinas que inicia la IU, suele ser correcto iniciarlas en Dispatchers.Main, que es el subproceso principal en Android. Una corrutina iniciada en Dispatchers.Main no bloqueará el subproceso principal mientras esté suspendida. Como una corrutina ViewModel casi siempre actualiza la IU en el subproceso principal, iniciar corrutinas en este subproceso te permitirá ahorrar más interruptores. Una corrutina iniciada en el subproceso principal puede cambiar de despachador en cualquier momento luego de que se inicia. Por ejemplo, puede usar otro despachador para analizar un resultado de JSON grande del subproceso principal.

Cómo usar viewModelScope

La biblioteca lifecycle-viewmodel-ktx de AndroidX agrega un CoroutineScope a los ViewModels configurados para iniciar corrutinas relacionadas con la IU. Para usar esta biblioteca, debes incluirla en el archivo build.gradle (Module: start) de tu proyecto. Este paso ya está realizado en los proyectos del codelab.

dependencies {
  ...
  implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:x.x.x"
}

La biblioteca agrega un elemento viewModelScope como función de extensión de la clase ViewModel. Este alcance está vinculado a Dispatchers.Main y se cancelará automáticamente cuando se borre el ViewModel.

Cómo pasar de subprocesos a corrutinas

En MainViewModel.kt, busca el próximo TODO junto con este código:

MainViewModel.kt

/**
* Wait one second then update the tap count.
*/
private fun updateTaps() {
   // TODO: Convert updateTaps to use coroutines
   tapCount++
   BACKGROUND.submit {
       Thread.sleep(1_000)
       _taps.postValue("$tapCount taps")
   }
}

Este código usa el BACKGROUND ExecutorService (definido en util/Executor.kt) para ejecutarse en un subproceso en segundo plano. Dado que sleep bloquea el subproceso actual, se congelará la IU si se llama al subproceso principal. Un segundo después de que el usuario hace clic en la vista principal, solicita una barra de notificaciones.

Esto es posible si quitas BACKGROUND del código y lo vuelves a ejecutar. El ícono giratorio de carga no se mostrará y todo lo demás pasará al estado final un segundo después.

MainViewModel.kt

/**
* Wait one second then update the tap count.
*/
private fun updateTaps() {
   // TODO: Convert updateTaps to use coroutines
   tapCount++
   Thread.sleep(1_000)
   _taps.postValue("$tapCount taps")
}

Reemplaza updateTaps por este código basado en corrutinas que haga lo mismo. Deberás importar launch y delay.

MainViewModel.kt

/**
* Wait one second then display a snackbar.
*/
fun updateTaps() {
   // launch a coroutine in viewModelScope
   viewModelScope.launch {
       tapCount++
       // suspend this coroutine for one second
       delay(1_000)
       // resume in the main dispatcher
       // _snackbar.value can be called directly from main thread
       _taps.postValue("$tapCount taps")
   }
}

Este código hace lo mismo: espera un segundo antes de mostrar una barra de notificaciones. Sin embargo, existen algunas diferencias importantes:

  1. viewModelScope.launch iniciará una corrutina en el viewModelScope. Esto significa que cuando se cancela el trabajo que pasamos a viewModelScope, se cancelan todas las corrutinas de este trabajo o alcance. Si el usuario dejó la actividad antes de que se muestre delay, esta corrutina se cancelará automáticamente cuando se llame a onCleared después de la destrucción del ViewModel.
  2. Dado que viewModelScope tiene un despachador predeterminado de Dispatchers.Main, esta corrutina se iniciará en el subproceso principal. Más adelante, veremos cómo usar diferentes subprocesos.
  3. La función delay es una función suspend. En Android Studio, esto se muestra con el ícono en el margen izquierdo. Si bien esta corrutina se ejecuta en el subproceso principal, delay no bloqueará el subproceso durante un segundo. En cambio, el despachador programará la corrutina para que se reanude en un segundo en la siguiente declaración.

Continúa y ejecútalo. Cuando haga clic en la vista principal, debería ver una barra de notificaciones un segundo más tarde.

En la próxima sección, analizaremos cómo probar esta función.

En este ejercicio, escribirás una prueba para el código que acabas de escribir. En este ejercicio, se muestra cómo probar las corrutinas que se ejecutan en Dispatchers.Main con la biblioteca kotlinx-coroutines-test. Más adelante en este codelab, implementarás una prueba que interactúa directamente con las corrutinas.

Revisa el código existente

Abre MainViewModelTest.kt en la carpeta androidTest.

MainViewModelTest.kt.

class MainViewModelTest {
   @get:Rule
   val coroutineScope =  MainCoroutineScopeRule()
   @get:Rule
   val instantTaskExecutorRule = InstantTaskExecutorRule()

   lateinit var subject: MainViewModel

   @Before
   fun setup() {
       subject = MainViewModel(
           TitleRepository(
                   MainNetworkFake("OK"),
                   TitleDaoFake("initial")
           ))
   }
}

Una regla es una forma de ejecutar código antes y después de la ejecución de una prueba en JUnit. Se usan dos reglas para probar MainViewModel en una prueba fuera del dispositivo:

  1. InstantTaskExecutorRule es una regla JUnit que configura LiveData para ejecutar cada tarea de manera síncrona
  2. MainCoroutineScopeRule es una regla personalizada en esta base de código que configura Dispatchers.Main para usar un TestCoroutineDispatcher de kotlinx-coroutines-test. Esto permite que las pruebas avancen un reloj virtual para las pruebas y el código use Dispatchers.Main en las pruebas de unidades.

En el método setup, se crea una instancia nueva de MainViewModel mediante pruebas falsas. Estas son implementaciones falsas de la red y la base de datos que se proporcionan en el código de inicio para ayudar a escribir pruebas sin usar la red o base de datos reales.

En esta prueba, las emulaciones solo son necesarias para satisfacer las dependencias de MainViewModel. Más adelante en este codelab, actualizarás los elementos falsos para que sean compatibles con las corrutinas.

Escribe una prueba que controle corrutinas

Agrega una prueba nueva que garantice que los toques se actualicen un segundo después de que se haga clic en la vista principal:

MainViewModelTest.kt.

@Test
fun whenMainClicked_updatesTaps() {
   subject.onMainViewClicked()
   Truth.assertThat(subject.taps.getValueForTest()).isEqualTo("0 taps")
   coroutineScope.advanceTimeBy(1000)
   Truth.assertThat(subject.taps.getValueForTest()).isEqualTo("1 taps")
}

Si llamas a onMainViewClicked, se iniciará la corrutina que acabamos de crear. Esta prueba verifica que el texto de los toques permanezca &tt;0 toques inmediatamente después de que se llame a onMainViewClicked, y luego, 1 segundo después, se actualice a 1 toques.

Esta prueba usa virtual-time para controlar la ejecución de la corrutina que inició onMainViewClicked. MainCoroutineScopeRule te permite pausar, reanudar o controlar la ejecución de corrutinas que se inician en Dispatchers.Main. En este caso, se llama a advanceTimeBy(1_000), que hace que el despachador principal ejecute inmediatamente las corrutinas programadas para reanudarse 1 segundo después.

Esta prueba es totalmente determinista, lo que significa que siempre se ejecutará de la misma manera. Además, como tiene el control total de la ejecución de corrutinas iniciadas en Dispatchers.Main, no tiene que esperar un segundo para que se establezca el valor.

Ejecuta la prueba existente

  1. Haz clic con el botón derecho en el nombre de la clase MainViewModelTest en el editor para abrir un menú contextual.
  2. En el menú contextual, selecciona execute.pngRun 'MainViewModelTest'
  3. En las ejecuciones futuras, puedes seleccionar esta configuración de prueba en la configuración junto al botón execute.png de la barra de herramientas. De forma predeterminada, la configuración se llamará MainViewModelTest.

Deberías ver la aprobación para la prueba. Además, debería tardar menos de un segundo en ejecutarse.

En el siguiente ejercicio, aprenderás a convertir desde API de devolución de llamada existentes para usar corrutinas.

En este paso, comenzarás a convertir un repositorio para usar corrutinas. Para ello, agregaremos corrutinas a ViewModel, Repository, Room y Retrofit.

Te recomendamos que comprendas qué es responsable de cada parte de la arquitectura antes de cambiarlas a corrutinas.

  1. MainDatabase implementa una base de datos con Room que guarda y carga un Title.
  2. MainNetwork implementa una API de red que recupera un título nuevo. Usa Retrofit para recuperar títulos. Retrofit está configurado para mostrar errores o datos simulados de forma aleatoria, pero se comporta de manera similar a si realiza solicitudes de red reales.
  3. TitleRepository implementa una sola API para recuperar o actualizar el título combinando datos de la red y la base de datos.
  4. MainViewModel representa el estado de la pantalla y controla los eventos. Indicará al repositorio que actualice el título cuando el usuario presione la pantalla.

Como la solicitud de red está controlada por eventos de IU y queremos iniciar una corrutina basada en ellos, el lugar natural para comenzar a usar las corrutinas es ViewModel.

La versión de la devolución de llamada

Abre MainViewModel.kt para ver la declaración de refreshTitle.

MainViewModel.kt

/**
* Update title text via this LiveData
*/
val title = repository.title


// ... other code ...


/**
* Refresh the title, showing a loading spinner while it refreshes and errors via snackbar.
*/
fun refreshTitle() {
   // TODO: Convert refreshTitle to use coroutines
   _spinner.value = true
   repository.refreshTitleWithCallbacks(object: TitleRefreshCallback {
       override fun onCompleted() {
           _spinner.postValue(false)
       }

       override fun onError(cause: Throwable) {
           _snackBar.postValue(cause.message)
           _spinner.postValue(false)
       }
   })
}

Se llama a esta función cada vez que el usuario hace clic en la pantalla y hace que el repositorio actualice el título y escriba el nuevo en la base de datos.

Esta implementación usa una devolución de llamada para realizar algunas acciones:

  • Antes de iniciar una consulta, muestra un ícono giratorio de carga con _spinner.value = true.
  • Cuando obtiene un resultado, borra el ícono giratorio de carga con _spinner.value = false.
  • Si recibe un error, le indica a la barra de notificaciones que muestre y borre el ícono giratorio.

Ten en cuenta que la devolución de llamada onCompleted no pasa el title. Debido a que escribimos todos los títulos en la base de datos de Room, la IU se actualiza al título actual observando un LiveData que Room actualiza.

En la actualización de las corrutinas, mantendremos el mismo comportamiento. Este es un buen patrón para usar una fuente de datos observable, como una base de datos Room, a fin de mantener la IU actualizada automáticamente.

La versión de corrutinas

Volvamos a escribir refreshTitle con corrutinas.

Como la usaremos de inmediato, crearemos una función de suspensión vacía en nuestro repositorio (TitleRespository.kt). Define una función nueva que use el operador suspend para indicarle a Kotlin que funciona con corrutinas.

TitleRepository.kt

suspend fun refreshTitle() {
    // TODO: Refresh from network and write to database
    delay(500)
}

Cuando termines este codelab, actualizarás la información para usar Retrofit y Room a fin de recuperar un título nuevo y escribirlo en la base de datos mediante corrutinas. Por ahora, solo pasará 500 milisegundos haciéndose pasar por trabajos y luego continuar.

En MainViewModel, reemplaza la versión de la devolución de llamada de refreshTitle por una que inicie una corrutina nueva:

MainViewModel.kt

/**
* Refresh the title, showing a loading spinner while it refreshes and errors via snackbar.
*/
fun refreshTitle() {
   viewModelScope.launch {
       try {
           _spinner.value = true
           repository.refreshTitle()
       } catch (error: TitleRefreshError) {
           _snackBar.value = error.message
       } finally {
           _spinner.value = false
       }
   }
}

Analicemos esta función:

viewModelScope.launch {

Al igual que la corrutina para actualizar el recuento de toques, comienza por lanzar una nueva corrutina en viewModelScope. Se usará Dispatchers.Main, que es adecuado. Aunque refreshTitle realizará una solicitud de red y una búsqueda en la base de datos, puede usar corrutinas para exponer una interfaz segura para el subproceso principal. Por lo tanto, será seguro llamarlo desde la conversación principal.

Como usamos viewModelScope, cuando el usuario se aleje de esta pantalla, se cancelará automáticamente el trabajo que inició esta corrutina. Esto significa que no realizará solicitudes de red adicionales ni consultas en la base de datos.

Las siguientes líneas de código llaman a refreshTitle en repository.

try {
    _spinner.value = true
    repository.refreshTitle()
}

Antes de que esta corrutina haga algo, inicia el ícono giratorio de carga y, luego, llama a refreshTitle como una función normal. Sin embargo, como refreshTitle es una función de suspensión, se ejecuta de manera diferente a una función normal.

No es necesario pasar una devolución de llamada. La corrutina se suspenderá hasta que refreshTitle la reanude. Aunque parece una llamada a función de bloqueo normal, esperará automáticamente hasta que se completen la búsqueda de red y base de datos antes de reanudar sin bloquear el subproceso principal.

} catch (error: TitleRefreshError) {
    _snackBar.value = error.message
} finally {
    _spinner.value = false
}

Las excepciones en las funciones de suspensión funcionan como los errores en las funciones normales. Si arrojas un error en una función de suspensión, se arrojará al emisor. Por lo tanto, aunque se ejecuten de forma bastante diferente, puedes usar los bloques try/catch normales para controlarlos. Esto es útil porque te permite confiar en la compatibilidad integrada del lenguaje para el manejo de errores en lugar de compilar un control de errores personalizado para cada devolución de llamada.

Si arrojas una excepción desde una corrutina, esa corrutina la cancelará de forma predeterminada. Esto significa que es fácil cancelar varias tareas relacionadas a la vez.

Por último, podemos asegurarnos de que el ícono giratorio siempre esté desactivado después de que se ejecute la consulta.

Vuelve a ejecutar la aplicación. Para ello, selecciona la configuración de inicio y, luego, presiona execute.png. Deberías ver un ícono giratorio de carga cuando presiones en cualquier lugar. El título seguirá siendo el mismo porque todavía no hemos conectado nuestra red ni nuestra base de datos.

En el siguiente ejercicio, actualizarás el repositorio para hacer tu trabajo.

En este ejercicio, aprenderás a cambiar el subproceso en el que se ejecuta una corrutina para implementar una versión de trabajo de TitleRepository.

Cómo revisar el código de devolución de llamada existente en refreshTitle

Abre TitleRepository.kt y revisa la implementación existente basada en devoluciones de llamada.

TitleRepository.kt

// TitleRepository.kt

fun refreshTitleWithCallbacks(titleRefreshCallback: TitleRefreshCallback) {
   // This request will be run on a background thread by retrofit
   BACKGROUND.submit {
       try {
           // Make network request using a blocking call
           val result = network.fetchNextTitle().execute()
           if (result.isSuccessful) {
               // Save it to database
               titleDao.insertTitle(Title(result.body()!!))
               // Inform the caller the refresh is completed
               titleRefreshCallback.onCompleted()
           } else {
               // If it's not successful, inform the callback of the error
               titleRefreshCallback.onError(
                       TitleRefreshError("Unable to refresh title", null))
           }
       } catch (cause: Throwable) {
           // If anything throws an exception, inform the caller
           titleRefreshCallback.onError(
                   TitleRefreshError("Unable to refresh title", cause))
       }
   }
}

En TitleRepository.kt, el método refreshTitleWithCallbacks se implementa con una devolución de llamada para comunicar el estado de carga y error al llamador.

Esta función realiza varias acciones para implementar la actualización.

  1. Cambiar a otra conversación con BACKGROUND ExecutorService
  2. Ejecuta la solicitud de red fetchNextTitle con el método execute() de bloqueo. Esto ejecutará la solicitud de red en el subproceso actual; en este caso, uno en BACKGROUND.
  3. Si el resultado es correcto, guárdalo en la base de datos con insertTitle y llama al método onCompleted().
  4. Si el resultado no fue exitoso, o si hay una excepción, llama al método onError para informar al emisor sobre la actualización con errores.

Esta implementación basada en devolución de llamada es segura para el subproceso principal porque no bloqueará el subproceso principal. Sin embargo, debe usar una devolución de llamada para informar al emisor cuando se complete el trabajo. También llama a las devoluciones de llamada en el subproceso BACKGROUND que también cambió.

Llamadas de bloqueo de llamadas desde corrutinas

Sin introducir corrutinas en la red o la base de datos, podemos hacer que este código sea seguro para el subproceso principal mediante corrutinas. Esto nos permitirá deshacernos de la devolución de llamada y pasar el resultado de vuelta al subproceso que la llamó inicialmente.

Puedes usar este patrón cada vez que necesites realizar bloqueos o trabajos que requieran mucha CPU desde una corrutina, como ordenar y filtrar una lista grande o leer desde el disco.

Para alternar entre cualquier despachador, las corrutinas utilizan withContext. Llamar a withContext cambia al otro despachador solo para la lambda y, luego, regresa al despachador que lo llamó con el resultado de ella.

De forma predeterminada, las corrutinas de Kotlin proporcionan tres despachadores: Main, IO y Default. El despachador IO está optimizado para trabajo de E/S, como leer desde la red o el disco, mientras que el despachador predeterminado está optimizado para tareas con uso intensivo de CPU.

TitleRepository.kt

suspend fun refreshTitle() {
   // interact with *blocking* network and IO calls from a coroutine
   withContext(Dispatchers.IO) {
       val result = try {
           // Make network request using a blocking call
           network.fetchNextTitle().execute()
       } catch (cause: Throwable) {
           // If the network throws an exception, inform the caller
           throw TitleRefreshError("Unable to refresh title", cause)
       }
      
       if (result.isSuccessful) {
           // Save it to database
           titleDao.insertTitle(Title(result.body()!!))
       } else {
           // If it's not successful, inform the callback of the error
           throw TitleRefreshError("Unable to refresh title", null)
       }
   }
}

Esta implementación usa llamadas de bloqueo para la red y la base de datos, pero es un poco más simple que la versión de devolución de llamada.

Este código igualmente utiliza llamadas de bloqueo. Llamar a execute() y insertTitle(...) bloqueará el subproceso en el que se ejecutará esta corrutina. Sin embargo, si cambias a Dispatchers.IO con withContext, bloqueamos uno de los subprocesos en el despachador de IO. La corrutina que llamó a esto, que posiblemente se ejecute en Dispatchers.Main, se suspenderá hasta que se complete la lambda withContext.

En comparación con la versión de devolución de llamada, existen dos diferencias importantes:

  1. withContext le devuelve el resultado al despachador que lo llamó, en este caso, Dispatchers.Main. La versión de devolución de llamada la llama en un subproceso del servicio de ejecutor de BACKGROUND.
  2. El emisor no tiene que pasar una devolución de llamada a esta función. Pueden depender de la suspensión y la reanudación para obtener el resultado o el error.

Vuelve a ejecutar la app

Si vuelves a ejecutar la app, verás que la nueva implementación basada en corrutinas está cargando resultados desde la red.

En el siguiente paso, integrarás corrutinas en Room y Retrofit.

Para continuar con la integración de corrutinas, usaremos la compatibilidad con las funciones de suspensión en la versión estable de Room y Retrofit. Luego, simplificaremos el código que acabamos de escribir de manera sustancial mediante las funciones de suspensión.

Corrutinas en Room

Primero, abre MainDatabase.kt y haz que insertTitle sea una función de suspensión:

MainDatabase.kt

// add the suspend modifier to the existing insertTitle

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertTitle(title: Title)

Cuando lo hagas, Room hará que tu búsqueda sea segura para el subproceso principal y la ejecutará en un subproceso en segundo plano de forma automática. Sin embargo, también significa que solo puedes llamar a esta consulta desde una corrutina.

Y eso es todo lo que tienes que hacer para usar las corrutinas de Room. Muy buena.

Corrutinas en Retrofit

A continuación, veremos cómo integrar corrutinas con Retrofit. Abre MainNetwork.kt y cambia fetchNextTitle por una función de suspensión.

MainNetwork.kt

// add suspend modifier to the existing fetchNextTitle
// change return type from Call<String> to String

interface MainNetwork {
   @GET("next_title.json")
   suspend fun fetchNextTitle(): String
}

Para usar funciones de suspensión con Retrofit, debes realizar dos acciones:

  1. Agrega un modificador de suspensión a la función
  2. Quita el wrapper Call del tipo que se muestra. Aquí se muestra String, pero también puedes mostrar un tipo de copia de seguridad compleja en JSON. Si aún deseas proporcionar acceso al Result completo, puedes mostrar Result<String> en lugar de String desde la función de suspensión.

Retrofit hará que las funciones de suspensión sean seguras para el subproceso principal de forma automática, por lo que podrás llamarlas directamente desde Dispatchers.Main.

Cómo usar Room y Retrofit

Ahora que Room y Retrofit admiten funciones de suspensión, podemos usarlas desde nuestro repositorio. Abre TitleRepository.kt y observa cómo el uso de funciones de suspensión simplifica en gran medida la lógica, incluso en comparación con la versión de bloqueo:

Título Repository.kt

suspend fun refreshTitle() {
   try {
       // Make network request using a blocking call
       val result = network.fetchNextTitle()
       titleDao.insertTitle(Title(result))
   } catch (cause: Throwable) {
       // If anything throws an exception, inform the caller
       throw TitleRefreshError("Unable to refresh title", cause)
   }
}

Vaya, eso es mucho más corto. ¿Qué pasó? Resulta que depender de la suspensión y la reanudación permite que el código sea mucho más corto. Retrofit nos permite usar tipos de datos que se muestran como String o un objeto User aquí, en lugar de un Call. Es seguro hacerlo, ya que dentro de la función de suspensión, Retrofit puede ejecutar la solicitud de red en un subproceso en segundo plano y reanudar la corrutina cuando se complete la llamada.

Aún mejor, eliminamos el withContext. Dado que Room y Retrofit proporcionan funciones de suspensión seguras para el subproceso principal, es seguro organizar este trabajo asíncrono desde Dispatchers.Main.

Cómo solucionar errores del compilador

El movimiento a las corrutinas implica el cambio de la firma de las funciones como no se puede llamar a una función de suspensión desde una función normal. Cuando agregaste el modificador suspend en este paso, se generaron algunos errores de compilador que muestran lo que sucedería si cambiaras una función para suspenderla en un proyecto real.

Revisa el proyecto y corrige los errores del compilador. Para ello, cambia la función que se debe suspender. Estas son las resoluciones rápidas para cada una:

TestingFakes.kt;

Actualiza los elementos falsos de prueba para admitir los nuevos modificadores de suspensión.

TítuloDaoFake

  1. Presiona Alt Intro y modifica los modificadores de suspensión en todas las funciones de la parte heiranchy.

MainNetworkFake;

  1. Presiona Alt Intro y modifica los modificadores de suspensión en todas las funciones de la parte heiranchy.
  2. Reemplaza fetchNextTitle por esta función
override suspend fun fetchNextTitle() = result

MainNetworkCompletableFake

  1. Presiona Alt Intro y modifica los modificadores de suspensión en todas las funciones de la parte heiranchy.
  2. Reemplaza fetchNextTitle por esta función
override suspend fun fetchNextTitle() = completable.await()

TitleRepository.kt

  • Borra la función refreshTitleWithCallbacks, dado que ya no se usa.

Ejecuta la app

Vuelve a ejecutar la app. Una vez que se compile, verás que carga datos con corrutinas desde ViewModel hasta Room y Retrofit.

¡Felicitaciones! Cambiaste por completo esta app a fin de usar corrutinas. Para terminar, hablaremos un poco sobre cómo probar lo que acabamos de hacer.

En este ejercicio, escribirás una prueba que llame directamente a una función suspend.

Dado que refreshTitle se expone como una API pública, se probará directamente y mostrará cómo llamar a las funciones de corrutinas a partir de pruebas.

Esta es la función refreshTitle que implementaste en el último ejercicio:

TitleRepository.kt

suspend fun refreshTitle() {
   try {
       // Make network request using a blocking call
       val result = network.fetchNextTitle()
       titleDao.insertTitle(Title(result))
   } catch (cause: Throwable) {
       // If anything throws an exception, inform the caller
       throw TitleRefreshError("Unable to refresh title", cause)
   }
}

Cómo escribir una prueba que llame a una función de suspensión

Abre TitleRepositoryTest.kt en la carpeta test, que tiene dos TODOS.

Llama a refreshTitle desde la primera prueba whenRefreshTitleSuccess_insertsRows.

@Test
fun whenRefreshTitleSuccess_insertsRows() {
   val subject = TitleRepository(
       MainNetworkFake("OK"),
       TitleDaoFake("title")
   )

   subject.refreshTitle()
}

Debido a que refreshTitle es una función suspend que Kotlin no sabe cómo llamarla, pero desde una corrutina o desde otra función de suspensión, recibirás un error de compilador, por lo que se debería llamar a la siguiente función: "Suspend refresh refreshTitle desde una corrutina o desde otra función de suspensión.

El ejecutor de pruebas no tiene información sobre las corrutinas para que no podamos hacer de esta prueba una función de suspensión. Podríamos launch una corrutina usando un CoroutineScope como en un ViewModel; sin embargo, las pruebas necesitan ejecutar corrutinas hasta su finalización antes de que se muestren. Una vez que se muestra una función de prueba, la prueba termina. Las corrutinas que se inician con launch son código asíncrono, que puede completarse en algún momento. Por lo tanto, para probar ese código asíncrono, necesitas algún método para indicarle a la prueba que espere hasta que se complete tu corrutina. Como launch es una llamada sin bloqueo, significa que se muestra de inmediato y puede seguir ejecutando una corrutina después de que se muestra la función. No se puede usar en pruebas. Por ejemplo:

@Test
fun whenRefreshTitleSuccess_insertsRows() {
   val subject = TitleRepository(
       MainNetworkFake("OK"),
       TitleDaoFake("title")
   )

   // launch starts a coroutine then immediately returns
   GlobalScope.launch {
       // since this is asynchronous code, this may be called *after* the test completes
       subject.refreshTitle()
   }
   // test function returns immediately, and
   // doesn't see the results of refreshTitle
}

Esta prueba a veces fallará. La llamada a launch se mostrará de inmediato y se ejecutará al mismo tiempo que el resto del caso de prueba. La prueba no tiene forma de saber si refreshTitle se ejecutó o no, y cualquier aserción como verificar que se haya actualizado la base de datos sería inestable. Además, si refreshTitle arroja una excepción, no se arrojará en la pila de llamadas de prueba. En su lugar, se arrojará en el controlador de excepciones no detectadas de GlobalScope.

La biblioteca kotlinx-coroutines-test tiene la función runBlockingTest que se bloquea mientras llama a las funciones de suspensión. Cuando runBlockingTest llama a una función de suspensión o a launches una corrutina nueva, la ejecuta de inmediato de forma predeterminada. Podríamos decir que es una manera de convertir corrutinas y funciones de suspensión en llamadas a funciones normales.

Además, runBlockingTest volverá a mostrar excepciones no detectadas. De esta manera, es más fácil probar cuando una corrutina está arrojando una excepción.

Cómo implementar una prueba con una corrutina

Une la llamada a refreshTitle con runBlockingTest y quita el wrapper GlobalScope.launch de subject.refreshTitle().

TitleRepositoryTest.kt

@Test
fun whenRefreshTitleSuccess_insertsRows() = runBlockingTest {
   val titleDao = TitleDaoFake("title")
   val subject = TitleRepository(
           MainNetworkFake("OK"),
           titleDao
   )

   subject.refreshTitle()
   Truth.assertThat(titleDao.nextInsertedOrNull()).isEqualTo("OK")
}

Esta prueba usa los elementos falsos proporcionados para verificar que refreshTitle esté insertado en la base de datos.

Cuando la prueba llame a runBlockingTest, se bloqueará hasta que se complete la corrutina iniciada por runBlockingTest. Luego, cuando llamamos a refreshTitle, se usa el mecanismo normal de suspensión y reanudación para esperar a que se agregue la fila de base de datos a nuestra instancia falsa.

Una vez que se completa la corrutina de prueba, se muestra runBlockingTest.

Cómo escribir una prueba de tiempo de espera

Queremos agregar un tiempo de espera breve a la solicitud de red. Primero, escribiremos la prueba y, luego, implementaremos el tiempo de espera. Crea una prueba nueva:

TitleRepositoryTest.kt

@Test(expected = TitleRefreshError::class)
fun whenRefreshTitleTimeout_throws() = runBlockingTest {
   val network = MainNetworkCompletableFake()
   val subject = TitleRepository(
           network,
           TitleDaoFake("title")
   )

   launch {
       subject.refreshTitle()
   }

   advanceTimeBy(5_000)
}

Esta prueba utiliza el MainNetworkCompletableFake falso proporcionado, que es un falso de la red diseñado para suspender a los emisores hasta que la prueba los continúe. Cuando refreshTitle intente realizar una solicitud de red, se detendrá definitivamente porque queremos probar tiempos de espera.

Luego, inicia una corrutina separada para llamar a refreshTitle. Esta es una parte clave de los tiempos de espera de prueba, que debe ocurrir en una corrutina diferente de la que crea runBlockingTest. De esta manera, podemos llamar a la siguiente línea, advanceTimeBy(5_000), que hará que el tiempo avance 5 segundos y que se agote el tiempo de espera de la otra corrutina.

Esta es una prueba de tiempo de espera completa que pasará una vez que implementemos el tiempo de espera.

Ejecuta el comando ahora y mira lo que sucede:

Caused by: kotlinx.coroutines.test.UncompletedCoroutinesError: Test finished with active jobs: ["...]

Una de las funciones de runBlockingTest es que no te permitirá filtrar corrutinas después de completar la prueba. Si hay corrutinas sin terminar, como nuestra corrutina de inicio, al final de la prueba, esta fallará.

Cómo agregar un tiempo de espera

Abre TitleRepository y agrega un tiempo de espera de cinco segundos a la recuperación de red. Puedes hacerlo mediante la función withTimeout:

TitleRepository.kt

suspend fun refreshTitle() {
   try {
       // Make network request using a blocking call
       val result = withTimeout(5_000) {
           network.fetchNextTitle()
       }
       titleDao.insertTitle(Title(result))
   } catch (cause: Throwable) {
       // If anything throws an exception, inform the caller
       throw TitleRefreshError("Unable to refresh title", cause)
   }
}

Ejecuta la prueba. Cuando ejecutes las pruebas, deberían aparecer todas.

En el siguiente ejercicio, aprenderás a escribir funciones de orden superior con corrutinas.

En este ejercicio, refactorizarás refreshTitle en MainViewModel para usar una función general de carga de datos. Esto te enseñará a compilar funciones de orden superior que usen corrutinas.

La implementación actual de refreshTitle funciona, pero podemos crear una corrutina de carga de datos general que siempre muestre el ícono giratorio. Esto puede ser útil en una base de código que carga datos en respuesta a varios eventos y desea garantizar que el ícono giratorio de carga se muestre de manera coherente.

Al revisar la implementación actual en cada línea, excepto repository.refreshTitle(), se usa código estándar para mostrar el ícono giratorio y los errores de visualización.

// MainViewModel.kt

fun refreshTitle() {
   viewModelScope.launch {
       try {
           _spinner.value = true
           // this is the only part that changes between sources
           repository.refreshTitle() 
       } catch (error: TitleRefreshError) {
           _snackBar.value = error.message
       } finally {
           _spinner.value = false
       }
   }
}

Usa corrutinas en funciones de orden superior

Agrega este código a MainViewModel.kt

MainViewModel.kt

private fun launchDataLoad(block: suspend () -> Unit): Job {
   return viewModelScope.launch {
       try {
           _spinner.value = true
           block()
       } catch (error: TitleRefreshError) {
           _snackBar.value = error.message
       } finally {
           _spinner.value = false
       }
   }
}

Ahora, refactoriza refreshTitle() para usar esta función de orden superior.

MainViewModel.kt

fun refreshTitle() {
   launchDataLoad {
       repository.refreshTitle()
   }
}

A través de la abstracción de la lógica en torno a la visualización de un ícono giratorio de carga y la aparición de errores, se simplificó el código real necesario para cargar datos. Mostrar un ícono giratorio o mostrar un error es algo que es fácil de generalizar para cualquier carga de datos, mientras que la fuente de datos real y el destino se deben especificar cada vez.

Para compilar esta abstracción, launchDataLoad toma un argumento block que es una lambda de suspensión. Una lambda de suspensión te permite llamar a funciones de suspensión. Así es como Kotlin implementa los compiladores de corrutinas launch y runBlocking que utilizamos en este codelab.

// suspend lambda

block: suspend () -> Unit

Para hacer una lambda de suspensión, comienza con la palabra clave suspend. La flecha de función y el tipo de datos que se muestra Unit completan la declaración.

A menudo no es necesario declarar tus propias lambdas de suspensión, pero pueden ser útiles para crear abstracciones de este tipo que encapsulen la lógica repetida.

En este ejercicio, aprenderás a usar código basado en corrutinas de WorkManager.

¿Qué es WorkManager?

En Android, hay muchas opciones para realizar trabajos diferibles en segundo plano. En este ejercicio, se muestra cómo integrar WorkManager con corrutinas. WorkManager es una biblioteca compatible, flexible y simple que permite realizar trabajos diferibles en segundo plano. WorkManager es la solución recomendada para estos casos de uso en Android.

WorkManager es parte de Android Jetpack y un componente de la arquitectura para trabajos en segundo plano que requieren una ejecución tanto oportunista como garantizada. La ejecución oportunista implica que WorkManager realizará el trabajo en segundo plano tan pronto como sea posible. La ejecución garantizada implica que WorkManager se encargará de la lógica a los efectos de iniciar tu trabajo en diferentes situaciones, incluso si sales de la app.

Por lo tanto, WorkManager es una buena opción para tareas que deben completarse en algún momento.

Algunos ejemplos de tareas que muestran un buen uso de WorkManager:

  • Subir registros
  • Aplicar filtros a imágenes y guardar la imagen
  • Sincronizar datos locales con la red de forma periódica

Cómo usar corrutinas con WorkManager

WorkManager proporciona diferentes implementaciones de su clase ListanableWorker base para diferentes casos de uso

La clase de trabajador más simple nos permite tener una operación síncrona ejecutada por WorkManager. Sin embargo, hasta ahora, a fin de convertir nuestra base de código para usar corrutinas y funciones de suspensión, la mejor manera de usar WorkManager es a través de la clase CoroutineWorker, que permite definir la función doWork() como una función de suspensión.

Para comenzar, abre RefreshMainDataWork. Ya extiende CoroutineWorker, y debes implementar doWork.

Dentro de la función suspend doWork, llama a refreshTitle() desde el repositorio y muestra el resultado apropiado.

Después de completar el comentario TODO, el código se verá de la siguiente manera:

override suspend fun doWork(): Result {
   val database = getDatabase(applicationContext)
   val repository = TitleRepository(network, database.titleDao)

   return try {
       repository.refreshTitle()
       Result.success()
   } catch (error: TitleRefreshError) {
       Result.failure()
   }
}

Ten en cuenta que CoroutineWorker.doWork() es una función de suspensión. A diferencia de la clase Worker más simple, este código NO se ejecuta en el ejecutor especificado en la configuración de WorkManager, sino que usa el despachador en el miembro coroutineContext (de forma predeterminada, Dispatchers.Default).

Cómo probar nuestro CoroutineWorker

No se debe completar ninguna base de código sin realizar pruebas.

WorkManager pone a tu disposición varias formas de probar tus clases Worker. Para obtener más información sobre la infraestructura de prueba original, consulta la documentación.

WorkManager v2.1 presenta un nuevo conjunto de API para admitir una forma más simple de probar clases ListenableWorker y, como consecuencia, CoroutineWorker. En nuestro código, usaremos una de estas nuevas API: TestListenableWorkerBuilder.

Para agregar nuestra nueva prueba, actualiza el archivo RefreshMainDataWorkTest en la carpeta androidTest.

El contenido del archivo es:

package com.example.android.kotlincoroutines.main

import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.work.ListenableWorker.Result
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.work.ListenableWorker.Result
import androidx.work.testing.TestListenableWorkerBuilder
import com.example.android.kotlincoroutines.fakes.MainNetworkFake
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4


@RunWith(JUnit4::class)
class RefreshMainDataWorkTest {

@Test
fun testRefreshMainDataWork() {
   val fakeNetwork = MainNetworkFake("OK")

   val context = ApplicationProvider.getApplicationContext<Context>()
   val worker = TestListenableWorkerBuilder<RefreshMainDataWork>(context)
           .setWorkerFactory(RefreshMainDataWork.Factory(fakeNetwork))
           .build()

   // Start the work synchronously
   val result = worker.startWork().get()

   assertThat(result).isEqualTo(Result.success())
}

}

Antes de llegar a la prueba, le avisaremos a WorkManager sobre la fábrica para que podamos insertar la red falsa.

La prueba usa el TestListenableWorkerBuilder para crear nuestro trabajador y, luego, podemos ejecutarlo llamando al método startWork().

WorkManager es solo un ejemplo de cómo se pueden usar corrutinas para simplificar el diseño de las API.

En este codelab, abarcamos los conceptos básicos que necesitarás para comenzar a usar corrutinas en tu app.

Tratamos los siguientes temas:

  • Cómo integrar corrutinas en apps para Android desde la IU y trabajos de WorkManager a fin de simplificar la programación asíncrona
  • Cómo usar corrutinas dentro de una ViewModel para recuperar datos de la red y guardarlos en una base de datos sin bloquear el subproceso principal
  • y cómo cancelar todas las corrutinas cuando finaliza ViewModel.

Para probar el código basado en corrutinas, abarcamos tanto el comportamiento de pruebas como la llamada directa a las funciones suspend desde las pruebas.

Más información

Consulta el codelab Corrutinas avanzadas con LiveData y flujo de Kotlin para obtener más información sobre el uso avanzado de corrutinas en Android.

Las corrutinas de Kotlin tienen muchas funciones que no se abarcaron en este codelab. Si te interesa obtener más información sobre las corrutinas de Kotlin, lee las guías de corrutinas publicadas por JetBrains. Consulta también Cómo mejorar el rendimiento de la app con las corrutinas de Kotlin para obtener más patrones de uso de corrutinas en Android.