DEV Community

Cover image for Kotlin Coroutines on Android 101
Armando Picón
Armando Picón

Posted on

Kotlin Coroutines on Android 101

Las corutinas son una de las características que gracias a Kotlin tenemos hoy en día y que busca simplificar la forma en la que se ejecutan tareas asíncronas. Este artículo tiene por objetivo hacer un repaso por los conceptos claves para entender cómo funciona.

  • Las corutinas actúan de forma similar; sin embargo, es importante recalcar que no son hilos.
  • Varias corutinas pueden ser ejecutadas en un mismo hilo.
  • Internamente se hace uso de un pool de hilos que nos proveerá el sistema.
  • Las funciones suspendidas o suspend functions es una variante de nuestras funciones regulares pero que pueden ser pausadas y resumidas en un momento posterior.
  • Para la ejecución de corutinas o funciones suspendidas es necesario establecer previamente el ámbito o scope en el que van a ser ejecutados (Ej. CoroutineScope)
  • Además del ámbito, la ejecución de una corutina necesita un punto de entrada que se establece mediante el uso de un coroutine builder (Ej. launch{} o async{}).
  • Para establecer en qué conjunto de hilo o hilos se ejecutarán nuestras corutinas y funciones suspendidas se dispone de los Dispatchers (Main, IO y Default).
  • Las funciones suspendidas solo pueden ejecutarse dentro de una corutina o desde otra función suspendida.

No te preocupes si por el momento todo lo que he señalado hasta aquí se lee como chino; vamos a ejemplificar estos puntos e irlos explicando poco a poco.

Agregar dependencias

Como primer paso para entrar al mundo de las corutinas debemos agregar las siguientes dependencias a nuestro proyecto. Toma en consideración que a la fecha de publicación de este artículo la versión existente es la 1.3.4, tal vez para cuando tú leas este artículo esta versión haya cambiado.

    def coroutines_version = "1.3.4"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
Enter fullscreen mode Exit fullscreen mode

Crear layout

Para centrarnos en probar las corutinas vamos a crear una interfaz sencilla que contenga un elemento de tipo Button con el id @+id/button_launch y un elemento TextView con el id @+id/text_result. ¿Cómo se deben distribuir en la pantalla? Es algo que dejaré a tu criterio.

Preparar nuestras funciones base

Antes de tocar las corutinas necesitaremos crear algunas funciones utilitarias como las que muestro a continuación. Las puedes agregar a tu implementación de tu clase MainActivity.kt (asumiendo que no le cambiaste el nombre por defecto al momento de crear tu Activity):

fun showResult (result: String){
    // Aquí vamos a aprovechar el uso de los synthetic imports para obtener
    // la referencia al textview que agregamos en nuestro UI directamente
    text_result.text = text_result.text.toString() + result + "\n"
}

fun log (message: String){
    // Emplearemos esta función para ayudarnos a identificar el hilo en el 
    // que se está ejecutando nuestra corutina. No siempre será el mismo hilo.
    println("[${Thread.currentThread().name}]: $message")
}
Enter fullscreen mode Exit fullscreen mode

A continuación, dentro de nuestra función onCreate() vamos a agregar la invocación a la función setOnClickListener() de nuestro botón.

button_launch.setOnClickListener {
   // Dentro vamos a escribir nuestra primera corutina
}
Enter fullscreen mode Exit fullscreen mode

Vamos con las corutinas

Vamos a escribir un ejemplo bastante común, vamos a simular la invocación a dos servicios que nos retornarán valores aleatorios.

Para conseguir esto primero escribiremos un par de funciones como te las presento a continuación, estas funciones van a escribir en los logs en qué hilo se está ejecutando, luego va a esperar un determinado tiempo en milisegundos y, finalmente, retornarán un resultado (estamos retornando valores String pero también podríamos retornar otro tipo de valor):

suspend fun getApiResult1 (): String {
    log("Get result from suspend function 1")
    delay(3000) // milisegundos
    return "result 1"
}

suspend fun getApiResult2 (): String {
    log("Get result from suspend function 2")
    delay(1500) // milisegundos
    return "result 2"
}
Enter fullscreen mode Exit fullscreen mode

De estas dos funciones suspendidas es importante notar el uso de la función delay() la cual cumple un rol similar a Thread.sleep() pero sin bloquear el hilo y debido a que en sí misma es también una función suspendida, la regla nos indica que solo se pueden ejecutar desde una corutina o dentro de otra función suspendida (se lee redundante pero es así).

Ahora vamos a armar la implementación de la corutina con su correspondiente punto de entrada:

button_launch.setOnClickListener {
    // Dentro vamos a escribir nuestra primera corutina
    CoroutineScope(Dispatchers.Main).launch {
        executeRequest()
    }
}
Enter fullscreen mode Exit fullscreen mode

Aquí también estamos haciendo uso de la función withContext() y mediante Dispatchers.Main le estamos indicando que la ejecución de esta corutina empezará en el hilo principal. Adicionalmente, hacemos uso de la función launch la cual marcará el inicio de la corutina.

Por razones ilustrativas vamos a agregar un par de funciones adicionales, primero esta:

suspend fun executeRequest() = withContext(Dispatchers.IO) {
    val result1 = getApiResult1()
    showResult(result1)
    val result2 = getApiResult2()
    showResult(result2)
}
Enter fullscreen mode Exit fullscreen mode

Aquí también estamos haciendo uso de la función withContext() el cual especificará que el bloque de código que encierra se va a suspender en el grupo de hilos que determina el Dispatcher, en este caso Dispatchers.IO, el cual está optimizado para la realización de tareas de networking y escritura en disco.

Si intentamos ejecutar el código hasta aquí (espero que lo intentes), seguro la aplicación se va a romper ¿alguna idea del por qué? Dale una vuelta o checka la excepción que salió en tu consola.

Only the original thread that created a view hierarchy can touch its views.

Pero ¿qué pasó? bueno, resulta que gracias al withContext(Dispatchers.IO) esta última función se está ejecutando en los hilos de IO y no en el Main Thread o Hilo de UI. Vamos, entonces, a corregir este problema agregando una última función:

suspend fun sendResultToMainThread(result: String) = withContext(Dispatchers.Main){
    log("Display $result")
    showResult(result)
}
Enter fullscreen mode Exit fullscreen mode

Si lo hiciste bien podrás apreciar lo siguiente en tu consola de Logcat:

[DefaultDispatcher-worker-1]: Get result from suspend function 1
[main]: Display result 1
[DefaultDispatcher-worker-1]: Get result from suspend function 2
[main]: Display result 2
Enter fullscreen mode Exit fullscreen mode

Y listo, podemos apreciar los cambios que se van haciendo a medida que vamos ejecutando cada paso de nuestra pequeña aplicación. Ojo que no hemos empleado un adecuado diseño y hemos puesto todo dentro del Activity, pero la intención es ilustrar cómo funcionan los cambios de contexto durante la ejecución de la corutina y las funciones suspendidas.

Si les gustó este artículo, podría continuar profundizando en el tema y repasar otros conceptos.

Top comments (0)