Introducción a las corrutinas en el Playground de Kotlin

1. Antes de comenzar

En este codelab, se presenta la simultaneidad, una habilidad fundamental que deben comprender los desarrolladores de Android para brindar una excelente experiencia del usuario. La simultaneidad implica realizar varias tareas en la app al mismo tiempo. Por ejemplo, tu app puede obtener datos de un servidor web o guardar datos del usuario en el dispositivo mientras responde a eventos de entrada del usuario y actualiza la IU según corresponda.

Para realizar tareas de manera simultánea en tu app, usarás las corrutinas de Kotlin. Las corrutinas permiten que se suspenda la ejecución de un bloque de código y, luego, se reanude para que se pueda realizar otra tarea mientras tanto. Las corrutinas facilitan la escritura de código asíncrono, lo que significa que no se necesita terminar una tarea por completo antes de iniciar la siguiente y que se permite que varias tareas se ejecuten de forma simultánea.

En este codelab, se explican algunos ejemplos básicos del Playground de Kotlin, en los que podrás adquirir experiencia práctica con corrutinas para familiarizarte con la programación asíncrona.

Requisitos previos

  • Poder crear un programa básico de Kotlin con una función main()
  • Contar con conocimientos de los conceptos básicos del lenguaje Kotlin, incluidas funciones y lambdas

Qué compilarás

  • Programa breve en Kotlin para aprender los conceptos básicos de las corrutinas y experimentar con ellas

Qué aprenderás

  • Cómo pueden las corrutinas de Kotlin simplificar la programación asíncrona
  • El objetivo de la simultaneidad estructurada y por qué es importante

Requisitos

2. Código síncrono

Programa sencillo

En el código síncrono, solo una tarea conceptual está en curso a la vez. Podemos pensar que es como una ruta lineal secuencial. Una tarea debe terminarse por completo antes de iniciar la siguiente. A continuación, se muestra un ejemplo de código síncrono.

  1. Abre el Playground de Kotlin.
  2. Reemplaza el código actual con el siguiente para compilar un programa que muestra un pronóstico del tiempo de clima soleado. En la función main(), primero mostramos el texto: Weather forecast y, luego, Sunny.
fun main() {
    println("Weather forecast")
    println("Sunny")
}
  1. Ejecuta el código. El resultado de la ejecución del código anterior debería ser el siguiente:
Weather forecast
Sunny

println() es una llamada síncrona porque la tarea de mostrar el texto en el resultado se completa antes de que la ejecución pueda pasar a la siguiente línea de código. Como cada llamada a la función en main() es síncrona, toda la función main() es síncrona. Si una función es síncrona o asíncrona está determinada por las partes que la componen.

Una función síncrona se muestra solo cuando la tarea se terminó por completo. Por lo tanto, después de ejecutar la última sentencia de print en main(), se completa toda la tarea. Se muestra la función main(), y finaliza el programa.

Cómo agregar un retraso

Supongamos que, para obtener el pronóstico del tiempo de clima soleado, se requiere una solicitud de red a un servidor web remoto. Para simular la solicitud de red, agrega un retraso en el código antes de mostrar que el pronóstico del tiempo es soleado.

  1. Primero, agrega import kotlinx.coroutines.* en la parte superior del código antes de la función main(). De esta manera, se importan las funciones que usarás desde la biblioteca de corrutinas de Kotlin.
  2. Modifica tu código para agregar una llamada a delay(1000), que retrasa la ejecución del resto de la función main() por 1000 milisegundos o 1 segundo. Inserta esta llamada a delay() antes de la sentencia print para Sunny.
import kotlinx.coroutines.*

fun main() {
    println("Weather forecast")
    delay(1000)
    println("Sunny")
}

En realidad, delay() es una función de suspensión especial que proporciona la biblioteca de corrutinas de Kotlin. En este punto, se suspenderá (o detendrá) la ejecución de la función main() y, luego, se reanudará una vez que finalice la duración especificada del retraso (en este caso, será un segundo).

Si intentas ejecutar tu programa en este momento, verás un error de compilación: Suspend function 'delay' should be called only from a coroutine or another suspend function.

Para aprender a usar corrutinas dentro del Playground de Kotlin, puedes unir tu código existente con una llamada a la función runBlocking() desde la biblioteca de corrutinas. runBlocking() ejecuta un bucle de evento, que puede manejar varias tareas a la vez y continúa con cada una de las tareas desde donde se interrumpió cuando está lista para reanudarse.

  1. Mueve el contenido existente de la función main() al cuerpo de la llamada a runBlocking {}. El cuerpo de runBlocking{} se ejecuta en una corrutina nueva.
import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("Weather forecast")
        delay(1000)
        println("Sunny")
    }
}

runBlocking() es síncrono, por lo que no se mostrará hasta que toda la tarea dentro de su bloque lambda esté completa. Es decir, esperará a que se complete la tarea en la llamada a delay() (hasta que transcurra un segundo) y, luego, continuará con la ejecución de la sentencia print Sunny. Una vez que se completa toda la tarea en la función runBlocking(), se muestra la función, que finaliza el programa.

  1. Ejecuta el programa. Este es el resultado:
Weather forecast
Sunny

El resultado es el mismo que antes. El código sigue siendo sincrónico: se ejecuta en línea recta y solo realiza una acción a la vez. Sin embargo, la diferencia ahora es que se ejecuta durante un período más largo debido al retraso.

El prefijo "co" de "corrutina" hace referencia a "cooperativa". El código coopera para compartir el bucle de eventos subyacente cuando se suspende para esperar algo, lo que permite que se ejecute otra tarea mientras tanto. (Por su parte, "rutina" hace referencia a un conjunto de instrucciones, como una función). En el caso de este ejemplo, la corrutina se suspende cuando llega a la llamada a delay(). Se puede realizar otra tarea en ese segundo cuando se suspende la corrutina (aunque en este programa, no hay otra tarea para realizar). Cuando termina el retraso, la corrutina se reanuda y puede continuar mostrando Sunny en el resultado.

Cómo suspender funciones

Si se vuelve más compleja la lógica real para realizar la solicitud de red para obtener los datos meteorológicos, es mejor que extraigas esa lógica en su propia función. Refactoricemos el código para ver su efecto.

  1. Extrae el código que simula la solicitud de red para los datos meteorológicos y muévelo a su propia función llamada printForecast(). Llama a printForecast() desde el código runBlocking().
import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("Weather forecast")
        printForecast()
    }
}

fun printForecast() {
    delay(1000)
    println("Sunny")
}

Si ejecutas el programa ahora, verás el mismo error de compilación que viste antes. Solo se puede llamar a una función suspend desde una corrutina o desde otra función suspend. Por lo tanto, define printForecast() como una función suspend.

  1. Agrega el modificador suspend justo antes de la palabra clave fun en la declaración de la función printForecast() para convertirla en una función de suspensión.
import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("Weather forecast")
        printForecast()
    }
}

suspend fun printForecast() {
    delay(1000)
    println("Sunny")
}

Recuerda que delay() es una función de suspensión, y ahora printForecast() también lo es.

Una función de suspensión es como una función normal, pero se puede suspender y reanudar más tarde. Para ello, solo se puede llamar a las funciones suspend desde otras funciones del mismo tipo que admitan esta capacidad.

Una función de suspensión puede contener cero o más puntos de suspensión. Un punto de suspensión es el lugar dentro de la función en el que se puede suspender la ejecución de la función. Una vez que se reanuda la ejecución, se reanuda desde donde se interrumpió por última vez en el código y continúa con el resto de la función.

  1. Para practicar, agrega otra función de suspensión a tu código, debajo de la declaración de la función printForecast(). Llama a esta nueva función de suspensión printTemperature(). Puedes simular que realiza una solicitud de red para obtener los datos sobre la temperatura del pronóstico del tiempo.

Dentro de la función, también retrasa la ejecución por 1000 milisegundos y, luego, muestra un valor de temperatura en la salida (por ejemplo, 30 grados Celsius). Puedes usar la secuencia de escape "\u00b0" para mostrar el símbolo de grado, °.

suspend fun printTemperature() {
    delay(1000)
    println("30\u00b0C")
}
  1. Llama a la nueva función printTemperature() desde tu código runBlocking() en la función main(). Este es el código completo:
import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("Weather forecast")
        printForecast()
        printTemperature()
    }
}

suspend fun printForecast() {
    delay(1000)
    println("Sunny")
}

suspend fun printTemperature() {
    delay(1000)
    println("30\u00b0C")
}
  1. Ejecuta el programa. El resultado debería ser el siguiente:
Weather forecast
Sunny
30°C

En este código, la corrutina se suspende primero con el retraso en la función de suspensión printForecast() y, luego, se reanuda después de ese retraso de un segundo. El texto Sunny se muestra en el resultado. La función printForecast() vuelve al llamador.

A continuación, se llama a la función printTemperature(). Esa corrutina se suspende cuando alcanza la llamada a delay() y, luego, se reanuda un segundo después y termina de mostrar el valor de temperatura en el resultado. La función printTemperature() completó toda la tarea y se muestra.

En el cuerpo de runBlocking(), no hay más tareas para ejecutar, por lo que se muestra la función runBlocking(), y finaliza el programa.

Como se mencionó anteriormente, runBlocking() es síncrono, y cada llamada en el cuerpo se realizará de forma secuencial. Ten en cuenta que una función de suspensión bien diseñada se muestra solo una vez que se complete toda la tarea. Como resultado, estas funciones de suspensión se ejecutan una tras otra.

  1. Opcional: Si deseas ver cuánto tiempo se tarda en ejecutar este programa con retrasos, puedes unir tu código en una llamada a measureTimeMillis() que mostrará el tiempo en milisegundos que tarda en ejecutar el bloque de código que se pasa. Agrega la sentencia de importación (import kotlin.system.*) para tener acceso a esta función. Muestra el tiempo de ejecución y divídelo por 1000.0 para convertir los milisegundos en segundos.
import kotlin.system.*
import kotlinx.coroutines.*

fun main() {
    val time = measureTimeMillis {
        runBlocking {
            println("Weather forecast")
            printForecast()
            printTemperature()
        }
    }
    println("Execution time: ${time / 1000.0} seconds")
}
suspend fun printForecast() {
    delay(1000)
    println("Sunny")
}

suspend fun printTemperature() {
    delay(1000)
    println("30\u00b0C")
}

Resultado:

Weather forecast
Sunny
30°C
Execution time: 2.128 seconds

El resultado muestra que la ejecución tardó alrededor de 2.1 segundos. (El tiempo de ejecución exacto puede ser un poco diferente). Parece razonable, ya que cada una de las funciones de suspensión tiene un retraso de un segundo.

Hasta ahora, observaste que el código en una corrutina se invoca de forma secuencial y predeterminada. Debes declarar explícitamente si deseas que las tareas se ejecuten de forma simultánea, y aprenderás a hacerlo en la siguiente sección. Usarás el bucle de eventos cooperativos para realizar varias tareas de forma simultánea, lo que acelerará el tiempo de ejecución del programa.

3. Código asíncrono

launch()

Usa la función launch() de la biblioteca de corrutinas para iniciar una nueva corrutina. Para ejecutar tareas de forma simultánea, agrega varias funciones launch() a tu código, de modo que varias corrutinas estén en curso al mismo tiempo.

Las corrutinas en Kotlin siguen un concepto clave que se denomina simultaneidad estructurada, en el que tu código es secuencial de forma predeterminada y coopera con un bucle de evento subyacente, a menos que solicites una ejecución simultánea de manera explícita (p. ej., con launch()). Se supone que, si llamas a una función, esta debe terminar su tarea por completo en el momento en que se muestre, sin importar cuántas corrutinas haya usado en los detalles de implementación. Incluso si falla con una excepción, una vez que se arroja dicha excepción, no hay más tareas pendientes de la función. Por lo tanto, todo el trabajo se completa una vez que el flujo de control regresa de la función, ya sea que haya arrojado una excepción o completado su trabajo con éxito.

  1. Comienza con tu código desde los pasos anteriores. Usa la función launch() para mover cada llamada a printForecast() y printTemperature(), respectivamente, en sus propias corrutinas.
import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("Weather forecast")
        launch {
            printForecast()
        }
        launch {
            printTemperature()
        }
    }
}

suspend fun printForecast() {
    delay(1000)
    println("Sunny")
}

suspend fun printTemperature() {
    delay(1000)
    println("30\u00b0C")
}
  1. Ejecuta el programa. Este es el resultado:
Weather forecast
Sunny
30°C

El resultado es el mismo, pero tal vez hayas notado que es más rápido ejecutar el programa. Antes, se debía esperar a que la función de suspensión printForecast() finalizara por completo antes de pasar a la función printTemperature(). Ahora printForecast() y printTemperature() pueden ejecutarse de forma simultánea porque están en corrutinas diferentes.

La sentencia println (Weather Forecast) está en un cuadro en la parte superior del diagrama. Debajo, hay una flecha vertical que apunta directamente hacia abajo. De esa flecha vertical se desprende una flecha hacia la derecha que apunta a un cuadro que contiene la sentencia printForecast(). De a esa flecha vertical original, también hay otra rama que va a la derecha, con una flecha que apunta a un cuadro que contiene la sentencia printTemperature().

La llamada a launch { printForecast() } se puede mostrar antes de que se complete toda la tarea en printForecast(). Esa es la belleza de las corrutinas. Puedes pasar a la siguiente llamada a launch() para iniciar la siguiente corrutina. De manera similar, launch { printTemperature() } también se muestra incluso antes de que se complete toda la tarea.

  1. Opcional: Si deseas ver cuánto más rápido es ahora el programa, puedes agregar el código measureTimeMillis() para verificar el tiempo de ejecución.
import kotlin.system.*
import kotlinx.coroutines.*

fun main() {
    val time = measureTimeMillis {
        runBlocking {
            println("Weather forecast")
            launch {
                printForecast()
            }
            launch {
                printTemperature()
            }
        }
    }
    println("Execution time: ${time / 1000.0} seconds")
}

...

Resultado:

Weather forecast
Sunny
30°C
Execution time: 1.122 seconds

Puedes ver que el tiempo de ejecución ha disminuido aproximadamente de 2.1 segundos a 1.1 segundos, por lo que es más rápido ejecutar el programa una vez que agregas operaciones simultáneas. Puedes quitar este código de medición de tiempo antes de continuar con los siguientes pasos.

¿Qué crees que sucede si agregas otra sentencia print después de la segunda llamada a launch(), antes del final del código runBlocking()? ¿Dónde aparecería ese mensaje en el resultado?

  1. Modifica el código runBlocking() para agregar una sentencia print adicional antes del final de ese bloque.
...

fun main() {
    runBlocking {
        println("Weather forecast")
        launch {
            printForecast()
        }
        launch {
            printTemperature()
        }
        println("Have a good day!")
    }
}

...
  1. Ejecuta el programa. Este es el resultado:
Weather forecast
Have a good day!
Sunny
30°C

En este resultado, puedes observar que, después de iniciar las dos corrutinas nuevas para printForecast() y printTemperature(), puedes continuar con la siguiente instrucción que muestra Have a good day!. Esto demuestra la naturaleza de "activar y olvidar" de launch(). Activas una corrutina nueva con launch() y no tienes que preocuparte por el momento en que termina la tarea.

Luego, las corrutinas completarán la tarea y mostrarán las sentencias de resultado restantes. Una vez que se completó toda la tarea (incluidas todas las corrutinas) en el cuerpo de la llamada a runBlocking(), se muestra runBlocking(), y finaliza el programa.

Ahora cambiaste tu código síncrono a asíncrono. Cuando se muestra una función asíncrona, es posible que la tarea aún no se haya completado. Esto es lo que observaste en el caso de launch(). Se mostró la función, pero la tarea aún no se completó. Si usas launch(), se pueden ejecutar varias tareas en tu código de forma simultánea, lo que es una capacidad eficaz que puedes usar en las apps para Android que desarrollas.

async()

En el mundo real, no sabrás cuánto tardarán las solicitudes de red para el pronóstico y la temperatura. Si deseas mostrar un informe meteorológico unificado cuando se completan ambas tareas, el enfoque actual con launch() no es suficiente. Aquí es donde async() entra en juego.

Usa la función async() de la biblioteca de corrutinas si te interesa cuándo finaliza la corrutina y necesitas que esta muestre un valor.

La función async() muestra un objeto de tipo Deferred, que es como una promesa de que el resultado estará allí cuando esté listo. Puedes acceder al resultado en el objeto Deferred mediante await().

  1. Primero, cambia las funciones de suspensión para mostrar String en lugar de los datos sobre el pronóstico y la temperatura. Actualiza los nombres de las funciones printForecast() y printTemperature() por getForecast() y getTemperature().
...

suspend fun getForecast(): String {
    delay(1000)
    return "Sunny"
}

suspend fun getTemperature(): String {
    delay(1000)
    return "30\u00b0C"
}
  1. Modifica tu código runBlocking() a fin de que use async() en lugar de launch() para las dos corrutinas. Almacena el valor que se muestra de cada llamada a async() en variables que se denominan forecast y temperature, objetos Deferred que contienen el resultado del tipo String. (Especificar el tipo es opcional debido a la inferencia de tipo en Kotlin, pero se incluye a continuación para que puedas ver con mayor claridad qué muestra la llamada a async()).
import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("Weather forecast")
        val forecast: Deferred<String> = async {
            getForecast()
        }
        val temperature: Deferred<String> = async {
            getTemperature()
        }
        ...
    }
}

...
  1. Más adelante en la corrutina, después de las dos llamadas a async(), puedes acceder al resultado de esas corrutinas mediante una llamada a await() en los objetos Deferred. En este caso, puedes mostrar el valor de cada corrutina mediante forecast.await() y temperature.await().
import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("Weather forecast")
        val forecast: Deferred<String> = async {
            getForecast()
        }
        val temperature: Deferred<String> = async {
            getTemperature()
        }
        println("${forecast.await()} ${temperature.await()}")
        println("Have a good day!")
    }
}

suspend fun getForecast(): String {
    delay(1000)
    return "Sunny"
}

suspend fun getTemperature(): String {
    delay(1000)
    return "30\u00b0C"
}
  1. Ejecuta el programa. Se mostrará el siguiente resultado:
Weather forecast
Sunny 30°C
Have a good day!

¡Bravo! Creaste dos corrutinas que se ejecutaron de forma simultánea para obtener los datos sobre el pronóstico y la temperatura. Cuando cada una se completó, mostró un valor. Luego, combinaste los dos valores que se muestran en una sola sentencia print: Sunny 30°C.

Descomposición paralela

Podemos llevar este ejemplo del clima un paso más allá y ver cómo las corrutinas pueden ser útiles en la descomposición paralela de la tarea. La descomposición paralela implica tomar un problema y dividirlo en subtareas más pequeñas que se puedan resolver en paralelo. Cuando los resultados de las subtareas estén listos, podrás combinarlos en un resultado final.

En tu código, extrae la lógica del informe meteorológico del cuerpo de runBlocking() en una sola función getWeatherReport() que muestre la string combinada de Sunny 30°C.

  1. Define una nueva función de suspensión getWeatherReport() en tu código.
  2. Configura la función igual al resultado de una llamada a la función coroutineScope{} con un bloque lambda vacío que, finalmente, contendrá lógica para obtener el informe meteorológico.
...

suspend fun getWeatherReport() = coroutineScope {

}

...

coroutineScope{} crea un alcance local para esta tarea de informe meteorológico. Las corrutinas iniciadas dentro de este alcance se agrupan en su interior, lo que tiene implicaciones de cancelación y excepciones que aprenderás pronto.

  1. Dentro del cuerpo de coroutineScope(), crea dos corrutinas nuevas con async() para recuperar los datos sobre el pronóstico y la temperatura, respectivamente. Crea la string de informe meteorológico con la combinación de estos resultados de las dos corrutinas. Para ello, llama a await() en cada uno de los objetos Deferred que muestran las llamadas a async(). De esta manera, se garantiza que cada corrutina complete su tarea y muestre su resultado, antes de que regresemos de esta función.
...

suspend fun getWeatherReport() = coroutineScope {
    val forecast = async { getForecast() }
    val temperature = async { getTemperature() }
    "${forecast.await()} ${temperature.await()}"
}

...
  1. Llama a esta nueva función getWeatherReport() desde runBlocking(). Este es el código completo:
import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("Weather forecast")
        println(getWeatherReport())
        println("Have a good day!")
    }
}

suspend fun getWeatherReport() = coroutineScope {
    val forecast = async { getForecast() }
    val temperature = async { getTemperature() }
    "${forecast.await()} ${temperature.await()}"
}

suspend fun getForecast(): String {
    delay(1000)
    return "Sunny"
}

suspend fun getTemperature(): String {
    delay(1000)
    return "30\u00b0C"
}
  1. Ejecuta el programa y observarás el siguiente resultado:
Weather forecast
Sunny 30°C
Have a good day!

El resultado es el mismo, pero hay algunas conclusiones importantes aquí. Como se mencionó antes, coroutineScope() solo se mostrará una vez que se haya completado toda la tarea, incluidas las corrutinas que haya iniciado. En este caso, las corrutinas getForecast() y getTemperature() deben finalizar y mostrar sus resultados respectivos. Luego, se combinan el texto Sunny y 30°C, y se muestran desde el alcance. Este informe meteorológico de Sunny 30°C se muestra en el resultado, y el llamador puede pasar a la última sentencia print de Have a good day!.

Con coroutineScope(), si bien la función realiza, de forma interna, la tarea en simultáneo, al llamador le aparece como una operación síncrona porque coroutineScope no se mostrará hasta que se complete toda la tarea.

La idea clave aquí para la simultaneidad estructurada es que puedes tomar varias operaciones simultáneas y colocarlas en una sola operación síncrona, en la que la simultaneidad es un detalle de implementación. El único requisito sobre el código de llamada es estar en una función de suspensión o una corrutina. Aparte de eso, la estructura del código de llamada no necesita tener en cuenta los detalles de simultaneidad.

4. Excepciones y cancelación

Ahora, hablemos de algunas situaciones en las que se puede producir un error o cancelar una tarea.

Introducción a las excepciones

Una excepción es un evento inesperado que ocurre durante la ejecución del código. Debes implementar formas adecuadas de manejar estas excepciones para evitar que tu app falle y tenga un impacto negativo en la experiencia del usuario.

Este es un ejemplo de un programa que se cierra antes de tiempo con una excepción. El programa tiene como objetivo dividir numberOfPizzas / numberOfPeople para calcular la cantidad de pizzas que come cada persona. Supongamos que olvidas establecer un valor real de numberOfPeople por error.

fun main() {
    val numberOfPeople = 0
    val numberOfPizzas = 20
    println("Slices per person: ${numberOfPizzas / numberOfPeople}")
}

Cuando ejecutes el programa, fallará con una excepción aritmética, ya que no podrás dividir un número por cero.

Exception in thread "main" java.lang.ArithmeticException: / by zero
 at FileKt.main (File.kt:4)
 at FileKt.main (File.kt:-1)
 at jdk.internal.reflect.NativeMethodAccessorImpl.invoke0 (:-2)

Este problema tiene una solución directa, en la que puedes cambiar el valor inicial de numberOfPeople a un número distinto de cero. Sin embargo, a medida que tu código se vuelve más complejo, hay ciertos casos en los que no puedes anticipar ni evitar que ocurran todas las excepciones.

¿Qué sucede cuando falla una de tus corrutinas con una excepción? Modifica el código del programa del clima para averiguarlo.

Excepciones con corrutinas

  1. Comienza con el programa del clima de la sección anterior.
import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("Weather forecast")
        println(getWeatherReport())
        println("Have a good day!")
    }
}

suspend fun getWeatherReport() = coroutineScope {
    val forecast = async { getForecast() }
    val temperature = async { getTemperature() }
    "${forecast.await()} ${temperature.await()}"
}

suspend fun getForecast(): String {
    delay(1000)
    return "Sunny"
}

suspend fun getTemperature(): String {
    delay(1000)
    return "30\u00b0C"
}

Dentro de una de las funciones de suspensión, arroja una excepción de forma intencional para ver cuál sería el efecto. Esto simula que se produjo un error inesperado cuando se recuperaban los datos del servidor, lo cual es razonable.

  1. En la función getTemperature(), agrega una línea de código que arroje una excepción. Escribe una expresión que se arroje con la palabra clave throw en Kotlin, seguida de una nueva instancia de una excepción que se extienda desde Throwable.

Por ejemplo, puedes arrojar un AssertionError y pasar una cadena de mensaje que describa el error con más detalle: throw AssertionError("Temperature is invalid"). Si se arroja esta excepción, se detiene la ejecución adicional de la función getTemperature().

...

suspend fun getTemperature(): String {
    delay(500)
    throw AssertionError("Temperature is invalid")
    return "30\u00b0C"
}

También puedes cambiar el retraso a 500 milisegundos para el método getTemperature(), de modo que sepas que la excepción se producirá antes de que la otra función getForecast() pueda completar su tarea.

  1. Ejecuta el programa para ver el resultado.
Weather forecast
Exception in thread "main" java.lang.AssertionError: Temperature is invalid
 at FileKt.getTemperature (File.kt:24)
 at FileKt$getTemperature$1.invokeSuspend (File.kt:-1)
 at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith (ContinuationImpl.kt:33)

Para comprender este comportamiento, debes saber que existe una relación superior-secundaria entre las corrutinas. Puedes iniciar una corrutina (conocida como secundaria) desde otra corrutina (superior). A medida que lanzas más corrutinas a partir de ellas, puedes crear una jerarquía completa.

La corrutina que ejecuta getTemperature() y la que ejecuta getForecast() son corrutinas secundarias de la misma corrutina superior. El comportamiento que observas con excepciones en las corrutinas se debe a la simultaneidad estructurada. Cuando una de las corrutinas secundarias falla con una excepción, se propaga hacia arriba. Se cancela la corrutina superior, que, a su vez, cancela cualquier otra corrutina secundaria (p. ej., la corrutina que ejecuta getForecast() en este caso). Por último, el error se propaga hacia arriba y el programa falla con AssertionError.

Excepciones de try-catch

Si sabes que ciertas partes de tu código pueden arrojar una excepción, entonces puedes encerrar ese código con un bloque try-catch. Puedes capturar la excepción y manejarla de forma más fluida en tu app; por ejemplo, puedes mostrarle al usuario un mensaje de error útil. Este es un fragmento de código de su aspecto:

try {
    // Some code that may throw an exception
} catch (e: IllegalArgumentException) {
    // Handle exception
}

Este enfoque también funciona en el caso del código asíncrono con corrutinas. Puedes usar una expresión try-catch para capturar y manejar excepciones en corrutinas. Esto se debe a que, con la simultaneidad estructurada, el código secuencial aún es código síncrono, por lo que el bloque try-catch seguirá funcionando de la misma manera prevista.

...

fun main() {
    runBlocking {
        ...
        try {
            ...
            throw IllegalArgumentException("No city selected")
            ...
        } catch (e: IllegalArgumentException) {
            println("Caught exception $e")
            // Handle error
        }
    }
}

...

Para familiarizarte con el manejo de excepciones, modifica el programa meteorológico para capturar la excepción que agregaste antes y, luego, imprime la excepción en el resultado.

  1. Dentro de la función runBlocking(), agrega un bloque try-catch alrededor del código que llama a getWeatherReport(). Imprime el error que se capturó y, luego, un mensaje que indique que el informe del clima no está disponible.
import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("Weather forecast")
        try {
            println(getWeatherReport())
        } catch (e: AssertionError) {
            println("Caught exception in runBlocking(): $e")
            println("Report unavailable at this time")
        }
        println("Have a good day!")
    }
}

suspend fun getWeatherReport() = coroutineScope {
    val forecast = async { getForecast() }
    val temperature = async { getTemperature() }
    "${forecast.await()} ${temperature.await()}"
}

suspend fun getForecast(): String {
    delay(1000)
    return "Sunny"
}

suspend fun getTemperature(): String {
    delay(500)
    throw AssertionError("Temperature is invalid")
    return "30\u00b0C"
}
  1. Ejecuta el programa. Ahora el error se maneja de forma más fluida y el programa puede terminar de ejecutarse correctamente.
Weather forecast
Caught exception in runBlocking(): java.lang.AssertionError: Temperature is invalid
Report unavailable at this time
Have a good day!

En el resultado, puedes observar que getTemperature() arroja una excepción. En el cuerpo de la función runBlocking(), encierras la llamada println(getWeatherReport()) con un bloque try-catch. Capturas el tipo de excepción que se esperaba (AssertionError en el caso de este ejemplo). Luego, imprimes la excepción en el resultado como "Caught exception" seguida de la cadena del mensaje de error. Para manejar el error, le indicas al usuario que el informe del clima no está disponible con una sentencia println() adicional: Report unavailable at this time.

Ten en cuenta que este comportamiento significa que, si hay una falla en la obtención de la temperatura, no habrá ningún informe del clima (incluso si se recuperó un pronóstico válido).

Según cómo quieras que se comporte tu programa, hay una forma alternativa de manejar la excepción en el programa meteorológico.

  1. Mueve el manejo de errores para que el comportamiento de try-catch realmente ocurra dentro de la corrutina que lanza async() para recuperar la temperatura. De esa manera, el informe del clima puede imprimir el pronóstico, incluso si falló la temperatura. Este es el código:
import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("Weather forecast")
        println(getWeatherReport())
        println("Have a good day!")
    }
}

suspend fun getWeatherReport() = coroutineScope {
    val forecast = async { getForecast() }
    val temperature = async {
        try {
            getTemperature()
        } catch (e: AssertionError) {
            println("Caught exception $e")
            "{ No temperature found }"
        }
    }

    "${forecast.await()} ${temperature.await()}"
}

suspend fun getForecast(): String {
    delay(1000)
    return "Sunny"
}

suspend fun getTemperature(): String {
    delay(500)
    throw AssertionError("Temperature is invalid")
    return "30\u00b0C"
}
  1. Ejecuta el programa.
Weather forecast
Caught exception java.lang.AssertionError: Temperature is invalid
Sunny { No temperature found }
Have a good day!

En el resultado, puedes ver que la llamada a getTemperature() falló con una excepción, pero el código dentro de async() pudo capturar esa excepción y manejarla de forma más fluida, ya que hizo que la corrutina mostrara una String que dice que no se encontró la temperatura. El informe del tiempo se puede imprimir con una pronóstico exitoso de Sunny. Falta la temperatura en el informe del clima, pero aparece un mensaje en el que se explica que no se encontró. Esta es una mejor experiencia del usuario que si el programa fallara con el error.

Una forma útil de pensar en este enfoque de manejo de errores es que async() es el productor cuando se inicia una corrutina con él. await() es el consumidor, ya que espera consumir el resultado de la corrutina. El productor realiza la tarea y produce un resultado. El consumidor consume el resultado. Si hay una excepción en el productor, el consumidor obtendrá esa excepción si no se maneja, y la corrutina fallará. Sin embargo, si el productor puede capturar y manejar la excepción, el consumidor no verá esa excepción y se mostrará un resultado válido.

Este es el código getWeatherReport() de nuevo como referencia:

suspend fun getWeatherReport() = coroutineScope {
    val forecast = async { getForecast() }
    val temperature = async {
        try {
            getTemperature()
        } catch (e: AssertionError) {
            println("Caught exception $e")
            "{ No temperature found }"
        }
    }

    "${forecast.await()} ${temperature.await()}"
}

En este caso, el productor (async()) pudo capturar y manejar la excepción y mostrar un resultado String de "{ No temperature found }". El consumidor (await()) recibe este resultado String y ni siquiera necesita saber que se produjo una excepción. Esta es otra opción para manejar de forma más fluida una excepción que esperas que pueda ocurrir en tu código.

Ahora aprendiste que las excepciones se propagan hacia arriba en el árbol de corrutinas, a menos que se manejen. También es importante tener cuidado cuando la excepción se propaga hasta la raíz de la jerarquía, lo que podría hacer fallar toda la app. Obtén más detalles sobre el manejo de excepciones en la entrada de blog Excepciones en corrutinas y el artículo Manejo de excepciones de corrutinas.

Cancelación

Un tema similar a las excepciones es la cancelación de corrutinas. Por lo general, esta situación la genera el usuario cuando un evento provoca que la app cancele la tarea que había iniciado.

Por ejemplo, supongamos que el usuario seleccionó una preferencia en la app para dejar de ver los valores de temperatura. Solo quiere conocer el pronóstico del clima (p. ej., Sunny), pero no la temperatura exacta. Por lo tanto, cancela la corrutina que actualmente obtiene los datos de temperatura.

  1. Primero, comienza con el siguiente código inicial (sin cancelación).
import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("Weather forecast")
        println(getWeatherReport())
        println("Have a good day!")
    }
}

suspend fun getWeatherReport() = coroutineScope {
    val forecast = async { getForecast() }
    val temperature = async { getTemperature() }
    "${forecast.await()} ${temperature.await()}"
}

suspend fun getForecast(): String {
    delay(1000)
    return "Sunny"
}

suspend fun getTemperature(): String {
    delay(1000)
    return "30\u00b0C"
}
  1. Después de un tiempo, cancela la corrutina que estaba recuperando la información de temperatura para que el informe del clima solo muestre el pronóstico. Cambia el valor que se muestra del bloque coroutineScope para que solo sea la cadena del pronóstico del clima.
...

suspend fun getWeatherReport() = coroutineScope {
    val forecast = async { getForecast() }
    val temperature = async { getTemperature() }

    delay(200)
    temperature.cancel()

    "${forecast.await()}"
}

...
  1. Ejecuta el programa. Ahora, el resultado es el siguiente. El informe del clima solo contiene el pronóstico Sunny, pero no la temperatura, porque se canceló esa corrutina.
Weather forecast
Sunny
Have a good day!

Lo que aprendiste aquí es que se puede cancelar una corrutina, y esto no afectará a otras corrutinas en el mismo alcance y no se cancelará la corrutina superior.

En esta sección, viste cómo se comportan las cancelaciones y las excepciones en las corrutinas y cómo se relacionan con la jerarquía de corrutinas. Aprendamos más sobre los conceptos formales detrás de las corrutinas para que puedas comprender cómo se unen todas las piezas importantes.

5. Conceptos de corrutinas

Cuando ejecutas la tarea de forma asíncrona o simultánea, hay preguntas que debes responder sobre cómo se ejecutará el trabajo, durante cuánto tiempo debe existir la corrutina, qué sucede si se cancela o falla con un error, y mucho más. Las corrutinas siguen el principio de simultaneidad estructurada, que te obliga a responder estas preguntas cuando usas corrutinas en tu código mediante una combinación de mecanismos.

Job

Cuando inicias una corrutina con la función launch(), se muestra una instancia de Job. Dicha instancia contiene un controlador o referencia de la corrutina para que puedas administrar su ciclo de vida

val job = launch { ... }

El trabajo se puede usar para controlar el ciclo de vida o la duración de la corrutina, por ejemplo, para cancelar la corrutina si ya no necesitas la tarea.

job.cancel()

Con un trabajo, puedes verificar si está activo, cancelado o completado. El trabajo se completa si la corrutina y las corrutinas que inició completaron toda la tarea. Ten en cuenta que la corrutina podría haberse completado por un motivo diferente, por ejemplo, porque se canceló o falló con una excepción, pero el trabajo todavía se considera completado en ese momento.

Los trabajos también realizan un seguimiento de la relación superior-secundaria entre las corrutinas.

Jerarquía de trabajos

Cuando una corrutina inicia otra, el trabajo que muestra la corrutina nueva se considera secundario del trabajo superior original.

val job = launch {
    ...

    val childJob = launch { ... }

    ...
}

Estas relaciones de superior y secundario forman una jerarquía de trabajos, en la que cada trabajo puede iniciar trabajos y así sucesivamente.

En este diagrama, se muestra una jerarquía de árbol de los trabajos. En la raíz de la jerarquía, se observa un trabajo superior. Tiene 3 trabajos secundarios denominados Child 1 Job, Child 2 Job y Child 3 Job. Child 1 Job tiene dos trabajos secundarios: Child 1a Job y Child 1b Job. Además, Child 2 Job tiene un solo trabajo secundario que se denomina Child 2a Job. Por último, Child 3 Job tiene dos trabajos secundarios: Child 3a Job y Child 3b Job.

Esta relación de superior y secundario es importante porque determinará cierto comportamiento para el trabajo secundario y el superior, y otros secundarios que pertenezcan al mismo trabajo superior. Viste este comportamiento en los ejemplos anteriores con el programa meteorológico.

  • Si se cancela un trabajo superior, también se cancelan sus trabajos secundarios.
  • Cuando se cancela un trabajo secundario con job.cancel(), este finaliza, pero no se cancela su superior.
  • Si un trabajo falla con una excepción, cancela a su superior con esa excepción, lo que se conoce como propagación del error en sentido ascendente (hasta el trabajo superior, el superior del superior y así sucesivamente) .

CoroutineScope

Por lo general, las corrutinas se inician en un CoroutineScope. De esta manera, se garantiza que no tengamos corrutinas que no estén administradas y se hayan perdido, lo que podría desperdiciar recursos.

launch() y async() son funciones de extensión en CoroutineScope. Llama a launch() o async() en el alcance para crear una corrutina nueva dentro de ese alcance.

Un CoroutineScope está vinculado a un ciclo de vida, que establece límites sobre la duración en la que estarán activas las corrutinas dentro de ese alcance. Si se cancela un alcance, el trabajo se cancela, y la cancelación se propaga a los trabajos secundarios. Si un trabajo secundario en el alcance falla con una excepción, otros trabajos secundarios y el trabajo superior se cancelan, y la excepción se vuelve a arrojar al llamador.

CoroutineScope en el Playground de Kotlin

En este codelab, usaste runBlocking() que proporciona un CoroutineScope para tu programa. También aprendiste a usar coroutineScope { } para crear un alcance nuevo dentro de la función getWeatherReport().

CoroutineScope en apps para Android

Android proporciona compatibilidad con el alcance de corrutinas en entidades que tienen un ciclo de vida bien definido, como Activity (lifecycleScope) y ViewModel (viewModelScope). Las corrutinas que se inician dentro de estos alcances cumplirán con el ciclo de vida de la entidad correspondiente, como Activity o ViewModel.

Por ejemplo, supongamos que inicias una corrutina en un Activity con el alcance de corrutinas proporcionado que se denomina lifecycleScope. Si se destruye la actividad, se cancelará lifecycleScope, y también se cancelarán automáticamente todas sus corrutinas secundarias. Solo debes decidir si la corrutina que sigue al ciclo de vida de Activity es el comportamiento que deseas.

En la app de Race Tracker para Android en la que trabajarás, aprenderás a establecer el alcance de tus corrutinas en función del ciclo de vida de un elemento componible.

Detalles de implementación de CoroutineScope

Si revisas el código fuente para saber cómo se implementa CoroutineScope.kt en la biblioteca de corrutinas de Kotlin, puedes ver que CoroutineScope se declara como interfaz y contiene un CoroutineContext como variable.

Las funciones launch() y async() crean una nueva corrutina secundaria dentro de ese alcance, y el trabajo secundario también hereda el contexto desde el alcance. ¿Qué contiene el contexto? Analicemos esto a continuación.

CoroutineContext

CoroutineContext proporciona información sobre el contexto en el que se ejecutará la corrutina. En esencia, CoroutineContext es un mapa que almacena elementos en los que cada uno tiene una clave única. Estos campos no son obligatorios, pero estos son algunos ejemplos de lo que puede contener un contexto:

  • Nombre: Es el nombre de la corrutina para identificarla de forma única.
  • Trabajo: Controla el ciclo de vida de la corrutina.
  • Despachador: Envía la tarea al subproceso correspondiente.
  • Controlador de excepciones: Maneja las excepciones que arroja el código ejecutado en la corrutina.

Cada uno de los elementos en un contexto se puede agregar junto con el operador +. Por ejemplo, un CoroutineContext podría definirse de la siguiente manera:

Job() + Dispatchers.Main + exceptionHandler

Como no se proporciona un nombre, se usa el nombre predeterminado de la corrutina.

Dentro de una corrutina, si inicias una corrutina nueva, la corrutina secundaria heredará CoroutineContext de la corrutina superior, pero reemplazará el trabajo específicamente por la corrutina que se acaba de crear. También puedes anular cualquier elemento heredado del contexto superior si pasas argumentos a las funciones launch() o async() para las partes del contexto que deseas que sean diferentes.

scope.launch(Dispatchers.Default) {
    ...
}

Puedes obtener más información sobre CoroutineContext y cómo se hereda el contexto del trabajo superior en esta videoconferencia sobre KotlinConf.

Viste la mención del despachador varias veces. Su función es enviar o asignar la tarea a un subproceso. Analicemos los subprocesos y despachadores en más detalle.

Despachador

Las corrutinas usan despachadores para determinar el subproceso que se usará en su ejecución. Un subproceso, puede iniciarse, realiza una tarea (ejecuta parte del código) y, luego, finaliza cuando no hay más tareas para hacer.

Cuando un usuario inicia tu app, el sistema Android crea un nuevo proceso y un solo subproceso de ejecución para tu app, que se conoce como subproceso principal. El subproceso principal controla muchas operaciones importantes de tu app, incluidos los eventos del sistema Android, el dibujo de la IU en la pantalla, el manejo de eventos de entrada del usuario y mucho más. Como resultado, la mayor parte del código que escribas para tu app probablemente se ejecutará en el subproceso principal.

Existen dos términos que debes comprender cuando se trata del comportamiento de subprocesos de tu código: bloqueador y no bloqueador. Una función normal bloquea el subproceso de llamada hasta que se completa su trabajo. Esto significa que no genera el subproceso de llamada hasta que se completa el trabajo, por lo que no se podrá realizar ninguna otra tarea mientras tanto. Por el contrario, el código no bloqueador genera el subproceso de llamada hasta que se cumple una condición determinada, por lo que puedes realizar otras tareas mientras tanto. Puedes usar una función asíncrona para realizar trabajo no bloqueador, ya que se muestra antes de que se complete.

En el caso de las apps para Android, solo debes llamar al código bloqueador en el subproceso principal si se ejecutará con rapidez. El objetivo es mantener el subproceso principal desbloqueado para que pueda ejecutar el trabajo de inmediato si se activa un nuevo evento. Este proceso principal es el subproceso de IU para tus actividades y es responsable de los dibujos de la IU y los eventos relacionados con esta. Cuando se produce un cambio en la pantalla, se debe volver a dibujar la IU. Para realizar alguna acción similar a una animación en la pantalla, la IU debe volver a dibujarse con frecuencia para que aparezca como una transición fluida. Si el subproceso principal necesita ejecutar un bloque de tareas de larga duración, la pantalla no se actualizará con tanta frecuencia, y el usuario verá una transición abrupta (conocida como "bloqueo"), o la app podría bloquearse o tardar en responder.

Por lo tanto, debemos quitar los elementos de tareas de larga duración del subproceso principal y manejarlos en un subproceso diferente. Tu app comienza con un solo subproceso principal, pero puedes optar por crear varios subprocesos para realizar una tarea adicional. Estos subprocesos adicionales pueden denominarse subprocesos de trabajo. No hay problema si una tarea de larga duración bloquea un subproceso de trabajo durante mucho tiempo porque, mientras tanto, el subproceso principal está desbloqueado y puede responder al usuario de forma activa.

Kotlin proporciona algunos despachadores integrados:

  • Dispatchers.Main: Utiliza este despachador para ejecutar una corrutina en el subproceso principal de Android. Este despachador se usa principalmente para manejar interacciones y actualizaciones de la IU, así como realizar tareas rápidas.
  • Dispatchers.IO: Este despachador está optimizado para realizar E/S de disco o red fuera del subproceso principal. Por ejemplo, leer archivos o escribir en estos, y ejecutar cualquier operación de red.
  • Dispatchers.Default: Es un despachador predeterminado que se usa al llamar a launch() y async(), cuando no se especifica un despachador en su contexto. Puedes usar este despachador para realizar tareas intensivas a nivel computacional fuera del subproceso principal. Por ejemplo, el procesamiento de un archivo de imagen de mapa de bits.

Prueba el siguiente ejemplo en el Playground de Kotlin para comprender mejor los despachadores de corrutinas.

  1. Reemplaza cualquier código que tengas en el Playground de Kotlin con el siguiente código:
import kotlinx.coroutines.*

fun main() {
    runBlocking {
        launch {
            delay(1000)
            println("10 results found.")
        }
        println("Loading...")
    }
}
  1. Ahora, une el contenido de la corrutina iniciada con una llamada a withContext() para cambiar el CoroutineContext dentro del que se ejecuta la corrutina y anular el despachador de manera específica. Cambia a Dispatchers.Default (en lugar de Dispatchers.Main que, en la actualidad, se usa para el resto del código de corrutinas en el programa).
...

fun main() {
    runBlocking {
        launch {
            withContext(Dispatchers.Default) {
                delay(1000)
                println("10 results found.")
            }
        }
        println("Loading...")
    }
}

Es posible cambiar de despachador porque withContext() es una función de suspensión. Ejecuta el bloque de código proporcionado con un CoroutineContext nuevo. El contexto nuevo proviene del contexto del trabajo superior (el bloque launch() externo), a excepción de que anula el despachador que se usa en el contexto superior con el que se especifica aquí: Dispatchers.Default. De esta manera, es posible pasar de ejecutar una tarea con Dispatchers.Main a usar Dispatchers.Default.

  1. Ejecuta el programa. El resultado debería ser el siguiente:
Loading...
10 results found.
  1. Llama a Thread.currentThread().name para agregar sentencias print a fin de ver en qué subproceso estás.
import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("${Thread.currentThread().name} - runBlocking function")
                launch {
            println("${Thread.currentThread().name} - launch function")
            withContext(Dispatchers.Default) {
                println("${Thread.currentThread().name} - withContext function")
                delay(1000)
                println("10 results found.")
            }
            println("${Thread.currentThread().name} - end of launch function")
        }
        println("Loading...")
    }
}
  1. Ejecuta el programa. El resultado debería ser el siguiente:
main @coroutine#1 - runBlocking function
Loading...
main @coroutine#2 - launch function
DefaultDispatcher-worker-1 @coroutine#2 - withContext function
10 results found.
main @coroutine#2 - end of launch function

En este resultado, puedes observar que la mayor parte del código se ejecuta en corrutinas en el subproceso principal. Sin embargo, para la parte de tu código en el bloque withContext(Dispatchers.Default), se ejecuta en una corrutina en un subproceso de trabajo del despachador predeterminado (que no es el subproceso principal). Ten en cuenta que, después de que se muestre withContext(), la corrutina vuelve a ejecutarse en el subproceso principal (como lo demuestra la sentencia de resultado main @coroutine#2 - end of launch function). En este ejemplo, se muestra que puedes cambiar el despachador si modificas el contexto que se usa para la corrutina.

Si tienes corrutinas que se iniciaron en el subproceso principal y deseas quitar ciertas operaciones fuera del subproceso principal, puedes usar withContext para cambiar el despachador que se utiliza para ese trabajo. Elige correctamente entre los despachadores disponibles: Main, Default y IO, según el tipo de operación que sea. Luego, esa tarea se puede asignar a un subproceso (o un grupo de subprocesos denominado conjunto de subprocesos) que se designa para ese fin. Las corrutinas pueden suspenderse a sí mismas, y el despachador también influye en la manera en que se reanudan.

Ten en cuenta que, cuando trabajas con bibliotecas populares, como Room y Retrofit (en esta unidad y en la siguiente), es posible que no tengas que cambiar, de forma explícita, el despachador si el código de la biblioteca ya se encarga de realizar esta tarea con un despachador de corrutinas alternativo como Dispatchers.IO.. En esos casos, las funciones suspend que revelan esas bibliotecas ya pueden ser seguras para el subproceso principal y se las puede llamar desde una corrutina que se ejecute en este tipo de subproceso. La biblioteca en sí se encargará de cambiar el despachador por uno que use subprocesos de trabajo.

Ahora tienes una descripción general de alto nivel de las partes importantes de las corrutinas y la función que desempeñan CoroutineScope, CoroutineContext, CoroutineDispatcher y Jobs para darles forma al ciclo de vida y al comportamiento de las corrutinas.

6. Conclusión

¡Buen trabajo en este desafiante tema de corrutinas! Aprendiste que las corrutinas son muy útiles porque su ejecución puede suspenderse, lo que libera el subproceso subyacente para que realice otra tarea y, luego, se puede reanudar la corrutina. De esta manera, puedes ejecutar operaciones simultáneas en tu código.

El código de corrutinas en Kotlin sigue el principio de simultaneidad estructurada. Es secuencial de forma predeterminada, por lo que debes ser explícito si deseas simultaneidad (p. ej., mediante launch() o async()). Con la simultaneidad estructurada, puedes tomar varias operaciones simultáneas y colocarlas en una sola operación síncrona, en la que la simultaneidad es un detalle de implementación. El único requisito sobre el código de llamada es estar en una función de suspensión o una corrutina. Aparte de eso, la estructura del código de llamada no necesita tener en cuenta los detalles de simultaneidad. De esta manera, se permite que tu código asíncrono sea más fácil de leer y razonar.

La simultaneidad estructurada realiza un seguimiento de cada una de las corrutinas iniciadas en tu app y garantiza que no se pierdan. Las corrutinas pueden tener una jerarquía: las tareas pueden iniciar subtareas, lo que, a su vez, puede iniciar subtareas. Los trabajos mantienen la relación de superior y secundario entre las corrutinas y te permiten controlar el ciclo de vida de las corrutinas.

El inicio, la finalización, la cancelación y el error son cuatro operaciones frecuentes en la ejecución de la corrutina. Para facilitar el mantenimiento de programas simultáneos, la simultaneidad estructurada define principios que forman la base de cómo se administran las operaciones frecuentes en la jerarquía:

  1. Inicio: Inicia una corrutina en un alcance que tenga un límite definido de la duración en la que permanecerá activa.
  2. Finalización: El trabajo no estará completo hasta que se completen sus trabajos secundarios.
  3. Cancelación: Esta operación debe propagarse en sentido descendente. Cuando se cancela una corrutina, también se deben cancelar las corrutinas secundarias.
  4. Error: Esta operación debe propagarse en sentido ascendente. Cuando una corrutina arroje una excepción, el trabajo superior cancelará todos sus trabajos secundarios, se cancelará a sí mismo y propagará la excepción a su trabajo superior. Esto continúa hasta que se detecta y controla el error. Garantiza que cualquier error en el código se informe de forma correcta y nunca se pierda.

Gracias a la experiencia práctica con corrutinas y la comprensión de los conceptos detrás de estas, ahora cuentas con más recursos para escribir código simultáneo en tu app para Android. Si usas corrutinas para la programación asíncrona, tu código es más fácil de leer y razonar, más sólido en situaciones de cancelaciones y excepciones, y ofrece una experiencia más óptima y responsiva para los usuarios finales.

Resumen

  • Las corrutinas te permiten escribir código de larga duración que se ejecuta de forma simultánea sin aprender un nuevo estilo de programación. La ejecución de una corrutina es secuencial por diseño.
  • Las corrutinas siguen el principio de simultaneidad estructurada, que ayuda a garantizar que la tarea no se pierda y se vincule a un alcance con un límite determinado de la duración en la que permanecerá activa. Tu código es secuencial de forma predeterminada y coopera con un bucle de eventos subyacente, a menos que solicites una ejecución simultánea de manera explícita (p. ej., mediante launch() o async()). Se supone que, si llamas a una función, esta debe terminar su tarea por completo (a menos que falle con una excepción) en el momento en que se muestre, sin importar cuántas corrutinas haya usado en los detalles de implementación.
  • El modificador suspend se usa para marcar una función cuya ejecución se puede suspender y reanudar más adelante.
  • A una función suspend solo se la puede llamar desde otra función de suspensión o una corrutina.
  • Puedes iniciar una corrutina nueva con las funciones de extensión launch() o async() en CoroutineScope.
  • Job desempeña un papel importante para garantizar la simultaneidad estructurada, ya que administra el ciclo de vida de las corrutinas y mantiene la relación de superior y secundario.
  • Un CoroutineScope controla la vida útil de las corrutinas a través de Job y aplica de manera forzosa la cancelación y otras reglas a sus trabajos secundarios y a los secundarios de estos de manera recurrente.
  • Un CoroutineContext define el comportamiento de una corrutina y puede incluir referencias a un trabajo y un despachador de corrutinas.
  • Las corrutinas usan un CoroutineDispatcher a fin de determinar los subprocesos que se usarán para su ejecución.

Más información