Introducción A Corrutinas En Kotlin

En este tutorial te introducirás al uso de corrutinas en Kotlin para comenzar a usar programación asíncrona en tus aplicaciones.

Programación Asíncrona

Por lo general escribimos código con el fin de que una línea sea ejecutada luego de que termine la anterior, pero en ciertos escenarios las tareas toman tanto tiempo que no es beneficioso esperar su finalización.

Normalmente usamos la capacidad multihilo de los lenguajes de programación como Java, para ejecutar las tareas de forma asíncrona (periodos intercalables de tiempo).

Creamos un hilo de trabajo separado que descargue archivos grandes, realice peticiones al servidor, consulte la base de datos, imprimir un documento, etc.

De esta forma la interfaz (o hilo principal) no se bloquea y la aplicación se ve fluida ante el usuario.

En el caso de Kotlin, tenemos las corrutinas como solución a la programación asíncrona.

Veamos de qué se trata.

Corrutinas En Kotlin

Una corrutina es un conjunto de sentencias que realizan una tarea específica, con la capacidad suspender o resumir su ejecución sin bloquear un hilo.

Esto permite que tengas diferentes corrutinas cooperando entre ellas, suspendiéndose y resumiéndose en puntos especificados por ti o por Kotlin.

No significa que exista un hilo por cada corrutina, al contrario, puedes ejecutar varias en un solo. Donde podrás crear tu propio procesamiento concurrente.

Este comportamiento de las corrutinas en Kotlin te permite:

  • Reducir recursos del sistema al evitar la creación de grandes cantidades de hilos
  • Facilitar el retorno de datos de una tarea asíncrona
  • Facilitar el intercambio de datos entre tareas asíncronas

Añadir Corrutinas A Proyecto IntelliJ IDEA

Las corrutinas hacen parte del paquete kotlinx.coroutines, por lo que necesitas especificar la dependencia en la configuración de build.gradle.kts en tus proyectos IntelliJ IDEA.

dependencies {
    /* Otras dependencias */
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2")
}

Luego añade al bloque repositories(), la siguiente indicación:

repositories {
    jcenter()
}

Finaliza sincronizando tu proyecto. Una vez se complete la sincronización, ya puedes iniciar corrutinas.

Iniciar Un Corrutina Con launch()

Para iniciar una corrutina debes usar un constructor de corrutinas (launch(), runBlocking(), async(), etc.) y pasar un lambda con las sentencias a ejecutar como bloque de código.

Por ejemplo, simulemos un programa que marca las palabras encontradas por un usuario en una sopa de letras. Al mismo tiempo le temporizamos 10 segundos como plazo.

// Importar componentes de corrutinas
import kotlinx.coroutines.*

fun main() {
    // 1. Inicio
    println("¡Go!")

    // 2. Buscar palabras en el background
    GlobalScope.launch {
        (1..5).forEach {
            delay(300)
            println("¡Palabra $it encontrada!")
        }
    }

    // 3. Iniciar temporizador en foreground
    for (i in 10 downTo 1) {
        println("${i}s")
        Thread.sleep(100)
    }

    // 4. Tiempo fuera
    println("Se terminó el tiempo")
}

La solución propuesta consiste en:

  • Iniciar la corrutina con GlobalScope.launch{}. Esta cuenta las palabras encontradas. Usamos la función de suspención delay() para simular que el usuario se demora 300 milisegundos encontrando cada palabra
  • Iniciar un bucle for en reversa para simular el temporizador de 10 a 1. Usamos el método Thread.sleep() de la librería estándar de Java, para dormir el hilo principal (main)

Al ejecutar la aplicación se imprimirá:

¡Go!
10s
9s
8s
7s
¡Palabra 1 encontrada!
6s
5s
4s
¡Palabra 2 encontrada!
3s
2s
1s
¡Palabra 3 encontrada!
Se terminó el tiempo

Como ves en la salida, la esperanza de vida de la corrutina se extendió solo hasta que terminó el último println(). Por eso faltaron 2 palabras por encontrar.

Iniciar Una Corrutina Con runBlocking()

runBlocking() inicia una nueva corrutina y bloquea su hilo contenedor, hasta que se ejecuten todas las sentencias de su bloque de código.

Por ejemplo, como faltaron 2 palabras en el ejemplo anterior, podemos usar runBlocking() al final para crear una corrutina que brinde unos 600 milisegundos más, mientras el usuario encuentra las palabras:

// 4. Tiempo fuera
println("Se terminó el tiempo")
runBlocking {
    delay(600)
}

Al ejecutar el programa verás que se termina la corrutina inicial.

¡Go!
10s
9s
8s
7s
¡Palabra 1 encontrada!
6s
5s
4s
¡Palabra 2 encontrada!
3s
2s
1s
¡Palabra 3 encontrada!
Se terminó el tiempo
¡Palabra 4 encontrada!
¡Palabra 5 encontrada!

Lo anterior es equivalente a declarar main() como una función de única expresión, asignándole el resultado de runBlocking en línea:

fun main() = runBlocking<Unit> {
    // 1. Inicio
    println("¡Go!")

    // 2. Buscar palabras en el background
    GlobalScope.launch {
        (1..5).forEach {
            delay(300)
            println("¡Palabra $it encontrada!")
        }
    }

    // 3. Iniciar temporizador en foreground
    for (i in 10 downTo 1) {
        println("${i}s")
        delay(100)
    }

    // 4. Tiempo fuera
    println("Se terminó el tiempo")

    delay(600)
}

De esta forma tratamos a main() como corrutina y llamamos a delay() para la suspensión en el temporizador.

Por otro lado, ya que Unit puede ser inferido por el compilador, puedes omitirlo de la declaración:

fun main() = runBlocking {
}

Esperar A Un Job

Hasta el momento usábamos delay(600) para conseguir el resultado de la cuenta regresiva y la búsqueda de palabras. Esto permitió cohesionar ambos resultados sin problemas.

Sin embargo, el método Job.join() llega al mismo resultado por nosotros.

fun main() = runBlocking {
    // 1. Inicio
    println("¡Go!")

    // 2. Buscar palabras en el background
    val job= GlobalScope.launch {
        (1..5).forEach {
            delay(300)
            println("¡Palabra $it encontrada!")
        }
    }

    /*...*/

    // Unir
    job.join()
}

El método join() suspende la corrutina principal hasta que la corrutina asociada a job se complete.

Alcance De Corrutinas

El uso de GlobalScope.launch() crea corrutinas de nivel superior, esto quiere decir que tienen la capacidad de vivir hasta que termine la aplicación y no pueden ser canceladas prematuramente .

Para reducir este alcance, Kotlin nos permite crear los espacios donde queremos que se dé la concurrencia.

Por ejemplo, cuando expresamos a main() como una corrutina con runBlocking, automáticamente se creó un alcance CoroutineScope para su bloque de código.

Por lo que hasta que las corrutinas en el interior de main() no se ejecuten, este no terminará.

Aprovechando esto, podemos omitir la llamada de GlobalScope y de join(), ya que ahora la búsqueda de palabras hace parte del mismo alcance.

fun main() = runBlocking {
    // 1. Inicio
    println("¡Go!")

    // 2. Buscar palabras en el background
    launch {
        (1..5).forEach {
            delay(300)
            println("¡Palabra $it encontrada!")
        }
    }

    // 3. Iniciar temporizador en foreground
    for (i in 10 downTo 1) {
        println("${i}s")
        delay(100)
    }

    // 4. Tiempo fuera
    println("Se terminó el tiempo")
}

Iniciar Una Corrutina Con async()

La función async{} crea una corrutina y retorna su resultado futuro como una instancia de Deferred<T>.

Deferred<T> posee una función miembro denominada await(), la cual espera hasta que se ejecuten las sentencias (sin bloquear un hilo) y así entregar el valor de la corrutina.

Por ejemplo:

Busquemos el tiempo total empleado por el usuario encontrando las palabras. Necesitaremos retornar un valor Long con los milisegundos que le tomó.

Para solucionarlo:

  1. Cambia launch{} por async{}
  2. Declara y asigna el valor de async{} a una variable
  3. Toma el tiempo inicial
  4. Usa como valor final la resta del tiempo final del inicial
  5. Obtén el tiempo con await() e imprímelo

En código tendrás:

import kotlinx.coroutines.*

fun main() = runBlocking {

    val totalTime = async {
        val t0 = System.currentTimeMillis()
        (1..5).forEach {
            delay(300)
            println("¡Palabra $it encontrada!")
        }
        System.currentTimeMillis() - t0
    }

    println("Tiempo empleado: ${totalTime.await()}")
}

En la salida tendrás un resultado similar a este:

¡Palabra 1 encontrada!
¡Palabra 2 encontrada!
¡Palabra 3 encontrada!
¡Palabra 4 encontrada!
¡Palabra 5 encontrada!
Tiempo empleado: 1512

Como ves, la sentencia println() no se ejecuta hasta que await() tenga el resultado diferido. Sin embargo el hilo no es bloqueado, solo se suspende la corrutina que actúa como alcance.

Iniciar Corrutina Con Alcance Personalizado

Crea tu propio alcance para correr corrutinas, con la función de suspensión coroutineScope{}. El cual se ejecutará hasta que todas sus sentencias sean completadas.

Y a diferencia de runBlocking{}, coroutineScope{} no bloquea un hilo, si no que se suspende.

fun main() = runBlocking {

    coroutineScope {
        val totalTime = async {
            val t0 = System.currentTimeMillis()
            (1..5).forEach {
                delay(300)
                println("¡Palabra $it encontrada!")
            }
            System.currentTimeMillis() - t0
        }.await()

        println("Tiempo empleado: ${totalTime}")
    }
}

Funciones De Suspensión

Una función de suspensión o suspending function se caracteriza por ejecutarse dentro de una corrutina o al interior de otra función de suspensión.

Usa el modificador suspend en la declaración de la función para marcarla como suspendible.

Obviamente, si intentas llamarlas desde otro contexto, obtendrás un error de compilación.

Suspend function 'userSearchWords' should be called only from a coroutine or another suspend function

Por ejemplo:

Mueve las instrucciones de búsqueda del constructor sync{} a una suspending function llamada userSearchWords():

private suspend fun userSearchWords(): Long {
    val t0 = System.currentTimeMillis()
    (1..5).forEach {
        delay(300)
        println("¡Palabra $it encontrada!")
    }
    return System.currentTimeMillis() - t0
}

Al usar suspend es posible llamarla al interior del constructor de corrutina:

import kotlinx.coroutines.*

fun main() = runBlocking {

    val totalTime = async {
        userSearchWords()
    }.await()
    println("Tiempo empleado: ${totalTime}")
}

Marcarlas con este modificador le permite a Kotlin crear el ambiente y manejo necesario, para que hagan parte de las suspensiones de la corrutina donde se ejecutan.

Únete Al Discord De Develou

Si tienes problemas con el código de este tutorial, preguntas, recomendaciones o solo deseas discutir sobre desarrollo Android conmigo y otros desarrolladores, únete a la comunidad de Discord de Develou y siéntete libre de participar como gustes. ¡Te espero!