Kotlin Coroutines on Android 101
Tags: android, kotlin, coroutines
Published: 2020-04-27 06:25:32

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"
```

## 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")
}
```

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
}
```

## 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"
}
```

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()
    }
}
```

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)
}
```

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)
}
```

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
```

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.