Un primer vistazo a las co-rutinas de Kotlin en Android

Un primer vistazo a las co-rutinas de Kotlin en Android
Sin comentarios Facebook Twitter Flipboard E-mail

Las co-rutinas han sido la mayor incorporación en Kotlin 1.1. Son absolutamente geniales debido a su potencia, y la comunidad aún sigue descubriendo como aprovecharlas al máximo.

Explicado de forma simple, las co-rutinas son una manera de escribir código asíncrono de forma secuencial. En lugar de llenarlo todo de callbacks, puedes escribir tus líneas de código una detrás de otra. Algunas de ellas tendrán la capacidad de suspender la ejecución y esperar hasta que el resultado esté disponible.

Si te has formado como desarrollador de C#, async/wait es el concepto más parecido. Pero las co-rutinas en Kotlin son más potentes, porque en lugar de ser una implementación especifica de la idea, son una característica del lenguaje que puede implementarse de diferentes maneras para solucionar distintos problemas.

Las co-rutinas están basadas en la idea de funciones de suspensión: funciones que pueden parar la ejecución cuando son llamadas y reanudarla una vez que han terminado de ejecutar su propia tarea

Puedes escribir tu propia implementación, o usar una de las múltiples opciones que el equipo de Kotlin y otros desarrolladores independientes han construido.

Necesitas entender que las co-rutinas son una característica experimental en Kotlin 1.1. Esto quiere decir que la implementación podría cambiar en el futuro, y aunque la antigua aún será compatible, podrías querer migrar a la nueva definición. Como veremos después, necesitas optar por esta característica, de lo contrario verás un warning cuando la utilices.

Esto quiere decir que debes tomar este artículo como un ejemplo de que puedes hacer, no como una regla general. Las cosas pueden cambiar mucho en los próximos meses.

Entendiendo como trabajan las co-rutinas

Mi meta con este artículo es que seas capaz de tomar algunos conceptos básicos y ponerlos en práctica usando las librerías que hay, no construir tus propias implementaciones. Pero creo que es importante entender algo de cómo funcionan por dentro para que no te limites a usar ciegamente lo que te proveen las librerías.

Las co-rutinas están basadas en la idea de funciones de suspensión (suspending functions): funciones que pueden parar la ejecución cuando son llamadas y reanudarla una vez que han terminado de ejecutar su propia tarea.

Las funciones de suspensión se señalan con la palabra reservada suspend, y sólo pueden ser llamadas dentro de otras funciones de suspensión o de una co-rutina.

Esto significa que no puedes llamar a una función de suspensión en cualquier lugar. Es necesario que exista una función circundante que construya la co-rutina y proporcione el contexto requerido para que se haga. Algo como esto:

fun  async(block: suspend () -> T)

No voy a explicar cómo implementar la función de arriba. Es un proceso complejo que se sale del cometido de este artículo, y como comentaba ya hay soluciones implementadas que cubrirán la mayoría de los casos.

Si estás realmente interesado en construir la tuya, puedes leer la especificación escrita en el Github de co-rutinas. Sólo necesitas saber que la función puede tener cualquier nombre que quieras darle, y que tendrá al menos un bloque de suspensión como parámetro.

Entonces implementarías una función de suspensión y la llamarías dentro de ese bloque:

suspend fun mySuspendingFun(x: Int) : Result {
    ...
}

async { 
    val res = mySuspendingFun(20)
    print(res)
}

¿Entonces las co-rutinas son hilos? No exactamente. Funcionan de manera parecida, pero son mucho más ligeras y eficientes. Puedes tener millones de co-rutinas ejecutándose en unos pocos hilos, lo que abre un mundo de posibilidades.

Puedes usar las co-rutinas de tres maneras:

  • Implementación desde cero: esto implica construir tu propia manera de usar las co-rutinas. Es bastante complicado y por lo general no es necesario en absoluto.
  • Implementaciones de bajo nivel: Kotlin ofrece un conjunto de librerías que puedes encontrar en el repositorio kotlinx.coroutines, que resuelven algunas de las partes más difíciles y ofrecen una implementación especifica para diferentes escenarios. Hay una para Android, por ejemplo.
  • Implementaciones de más alto nivel: Si sólo buscas una solución que proporcione todo lo necesario para empezar a usar co-rutinas, hay muchas librerías que hacen todo el trabajo sucio por ti, y la lista no deja de crecer. Me voy a quedar con Anko, que proporciona una solución que funciona estupendamente con Android, y que probablemente te será familiar.

Usar Anko para las co-rutinas

Desde la versión 0.10, Anko ha proporcionado un par de maneras de usar co-rutinas en Android.

La primera es muy parecida a lo que hemos visto en el ejemplo de arriba, y también es similar a lo que hacen otras librerías.

Primero, necesitas crear un bloque asíncrono donde las funciones de suspensión puedan ser llamadas:

async(UI) {
    ...
}

El argumento UI es el contexto de la ejecución para el bloque async.

Entonces puedes crear bloques que sean ejecutados en un hilo en segundo plano y devolver el resultado al hilo de la UI. Estos bloques se definen usando la función bg:

async(UI) {
    val r1: Deferred = bg { fetchResult1() }
    val r2: Deferred = bg { fetchResult2() }
    updateUI(r1.await(), r2.await())
}

bg devuelve un objeto Deferred, el cual suspenderá la co-rutina cuando la función await() sea llamada, sólo hasta que el resultado sea devuelto. Usaremos esta solución en el ejemplo que hay más abajo.

Como puede que sepas, una de las características más interesantes cuando estás aprendiendo Kotlin, es que el compilador es capaz de inferir el tipo de las variables, así que esto puede hacerse más sencillo:

async(UI) {
    val r1 = bg { fetchResult1() }
    val r2 = bg { fetchResult2() }
    updateUI(r1.await(), r2.await())
}

La segunda alternativa es hacer uso de la integración con los listeners que proporcionan sub-librerias especificas, dependiendo del listener que vayas a utilizar.

Por ejemplo, en anko-sdk15-coroutines, existe un onClick listener cuya lambda es de hecho una co-rutina. Así que puedes empezar a usar funciones de suspensión inmediatamente dentro del bloque del listener.

textView.onClick {
    val r1 = bg { fetchResult1() }
    val r2 = bg { fetchResult2() }
    updateUI(r1.await(), r2.await())
}

Como puedes ver el resultado es bastante similar al anterior. Solo estás ahorrándote un poco de código.

Para usarla, necesitarás añadir algunas de estas dependencias, dependiendo del listener que quieras usar:

compile "org.jetbrains.anko:anko-sdk15-coroutines:$anko_version"
compile "org.jetbrains.anko:anko-appcompat-v7-coroutines:$anko_version"
compile "org.jetbrains.anko:anko-design-coroutines:$anko_version"

Usando co-rutinas en un ejemplo

En el ejemplo que desarrollo en el libro (que puedes encontrar aquí en Github), se crea una sencilla aplicación de tiempo.

Para usar las co-rutinas de Anko, primero necesitamos incluir la nueva dependencia:

compile "org.jetbrains.anko:anko-coroutines:$anko_version"

A continuación, si recuerdas, te dije que necesitabas activar esta característica, de lo contrario mostrará un warning. Para hacer esto, simplemente añade esta linea a el fichero gradle.properties en el directorio raíz (créalo si no existe aún):

kotlin.coroutines=enable

Ahora sí, ya tienes todo lo que necesitas para empezar a usar co-rutinas. Vamos primero a la actividad del detalle. Sólo llama a la base de datos (que se encarga de almacenar el pronostico semanal) utilizando un comando especifico.

Este es el código resultante:

async(UI) {
    val id = intent.getLongExtra(ID, -1)
    val result = bg { RequestDayForecastCommand(id).execute() }
    bindForecast(result.await())
}

¡Es estupendo! La previsión del tiempo se solicita en un hilo en segundo plano gracias a la función bg, que devuelve un resultado diferido. La espera de este resultado suspende la co-rutina por la llamada al await, hasta que el pronóstico esté listo para ser devuelvo.

Pero no todo es tan bonito. ¿Qué está pasando aquí? Las co-rutinas tienen un problema: están guardando una referencia a DetailActivity, provocando un leak si la petición nunca termina por ejemplo.

No te preocupes, ya que Anko tiene una solución. Puedes crear una referencia débil a tu actividad, y usarla en su lugar:

val ref = asReference()
val id = intent.getLongExtra(ID, -1)
async(UI) {
    val result = bg { RequestDayForecastCommand(id).execute() }
    ref().bindForecast(result.await())
}

Esta referencia permitirá llamar a la actividad cuando esté disponible, y cancelará la co-rutina en caso de que la actividad haya sido eliminada. Asegúrate de que todas las llamadas a métodos de la actividad o propiedades se realizan a través de este objeto ref.

Pero esto puede resultar un poco complicado si la co-rutina interactúa varias veces con la actividad. En MainActivity, por ejemplo, el uso de esta solución va a ser un poco más complicado.

Esta actividad llamará a un endpoint para solicitar un pronóstico semanal basado en un zipCode:

private fun loadForecast() {
    val ref = asReference()
    val localZipCode = zipCode
    async(UI) {
        val result = bg { RequestForecastCommand(localZipCode).execute() }
        val weekForecast = result.await()
        ref().updateUI(weekForecast)
    }
}

No puedes usar ref() dentro del bloque bg, porque el código dentro de ese bloque no es un contexto de suspensión, por lo que necesitas guardar el zipCode en otra variable local.

Personalmente creo que provocar un leak de la actividad durante 1-2 segundos no es tan malo, y probablemente no valdrá la pena el boilerplate. Por lo tanto, si puedes asegurarte de que tu proceso en segundo plano no puede durar para siempre (por ejemplo, estableciendo un tiempo máximo de espera en sus solicitudes de servidor), estarás a salvo de no usar asReference().

De esta manera, los cambios a MainActivity serían más simples:

private fun loadForecast() = async(UI) {
    val result = bg { RequestForecastCommand(zipCode).execute() }
    updateUI(result.await())
}

Así que con todo esto, ahora tienes tu código asíncrono escrito de forma síncrona fácilmente.

Este código es bastante simple, pero imagina otras situaciones complejas en las que el resultado de una operación en segundo plano es utilizado por la siguiente, o cuando necesitas iterar sobre una lista y ejecutar una solicitud por cada elemento.

Todo esto se puede escribir como código síncrono normal, que será mucho más fácil de leer y mantener.

Para simplificarlo, las co-rutinas son una manera de escribir código asíncrono secuencialmente

Aún hay mucho más que aprender para sacar el máximo provecho de las co-rutinas. Así que si tienes algo más de experiencia sobre esto, utiliza los comentarios para contarnos más sobre ello.

Comentarios cerrados
Inicio