Las corrutinas son una de las características más interesantes de Kotlin. Con ellas, se puede simplificar el trabajo de las tareas asíncronas de una manera impresionante y hacer que el código sea mucho más legible y fácil de entender.
Si prefieres verlo en vídeo, te dejo aquí un par de vídeos:
El primero, un resumen de 5 minutos sobre cómo funcionan las corrutinas:
El segundo, una sesión avanzada que hice de hora y media. Aquí cuento todo lo que se explica en el artículo:
Con las corrutinas, puedes escribir código asícrono, que se ha escrito tradicionalmente usando el patrón Callback, pero utilizando estilo síncrono. El valor de retorno de la función proporcionará el resultado de la llamada asíncrona.
¿Cómo ocurre esta magia? Lo veremos en un minuto. Pero primero entendamos por qué son necesarias las corrutinas.
Este contenido es una adaptación de mi training online Kotlin para Desarrolladores Android, que es un training certificado por JetBrains. ¡Aprende Kotlin desde cero en poco tiempo! Y por un tiempo muy limitado, consíguelo con un descuento del 40%. Al final del curso recibirás un certificiado de finalización.
Las corrutinas han estado con nosotros desde Kotlin 1.1 como una caraterística experimental. Pero Kotlin1.3 lanzó la API final, y ahora están listas para ser usadas en producción.
Objetivo de las corrutinas: El problema
Imagina que tienes una pantalla de inicio de sesión como la siguiete:
El usuario introduce un nombre de usuario, una contraseña y hace clic en Login.
Tu aplicación, por debajo,necesita hacer una solicitud al servidor para validar el inicio de sesión y otra llamada para recuperar una lista de amigos para mostrarla en la pantalla.
El código en Kotlin podría ser algo como esto:
progress.visibility = View.VISIBLE
userService.doLoginAsync(username, password) { user ->
userService.requestCurrentFriendsAsync(user) { friends ->
val finalUser = user.copy(friends = friends)
toast("User ${finalUser.name} has ${finalUser.friends.size} friends")
progress.visibility = View.GONE
}
}
Los pasos serían:
Muestra el progreso
Envía una solicitud al servidor para validar el inicio de sesión
Luego, con el resultado, realiza otra solicitud para recuperar la lista de amigos.
Finalmente, vuelve a ocultar el progreso.
Pero las cosas pueden se pueden poner más complicadas. Imagina que la API no es la mejor del mundo (estoy seguro de que has pasado por esto ?) y tienes que conseguir otro grupo de amigos: los amigos sugeridos. Después, necesitas fusionarlos en una lista única.
Tienes dos opciones aquí:
Hacer la segunda solicitud de amigos después de la primera, que es la forma mas simple pero no muy eficiente. La segunda solicitud no necesita el resultado de la primera.
Ejecutar ambas solicitudes al mismo tiempo y encontrar una manera de sincronizar los resultados de la devolución de llamada. Esto es bastante complejo.
En una App real, un programador perezoso (y pragmático) probablemente elegiría la primera:
El código comienza a ser díficil de entender, vemos el temido callback hell: la próxima llamada se realiza dentro del callback de la llamada anterior, por lo que el código se va indentando cada vez más.
Gracias a las lambdas en Kotlin, no tiene tan mala pinta. Pero quién sabe si, en un futuro, necesitarás hacer otra petición que haga que esto sea aún más difícil de manejar.
Además, recuerda que tomamos el camino fácil, que tampoco es muy eficiente.
¿Qué son las corrutinas?
Para entender fácilmente las corrutinas, digamos que las corrutinas son como hilos, pero mejores.
Primero, porque las corrutinas te permiten escribir tu código asícrono de forma secuencial, lo que reduce drásticamente la carga cognitiva.
Y segundo, porque son mucho más eficientes. Varias corrutinas se pueden ejecutar utilizando el mismo hilo. Por tanto, mientras que el número de hilos que se pueden ejecutar en una aplicación es bastante limitado, se pueden lanzar tantas corrutinas como se necesite. El límite es casi infinito.
Las corrutinas se basan en la idea de las funciones de suspensión. Estas son funciones que pueden detener la ejecución de una corrutina en cualquier punto y luego devolverle el control una vez que el resultado esté listo y la función haya terminado de hacer su trabajo.
Por lo tanto, las corrutinas son básicamente un lugar seguro donde las funciones de suspensión (normalmente) no bloquean el hilo principal.Y digo normalmente porque depende de cómo las definamos. Veremos todo esto más tarde.
coroutine {
progress.visibility = View.VISIBLE
val user = suspended { userService.doLogin(username, password) }
val currentFriends = suspended { userService.requestCurrentFriends(user) }
val finalUser = user.copy(friends = currentFriends)
toast("User ${finalUser.name} has ${finalUser.friends.size} friends")
progress.visibility = View.GONE
}
Así que en el ejemplo anterior, tenemos una estructura típica de una corrutina. Tendremos un constructor de corrutina, y un conjunto de funciones de suspensión que suspenderán la ejecución de la corrutina hasta que devuelvan el resultado.
Luego, puedes usar el resultado en la siguiente línea. Muy parecido al código secuencial. Estos dos artefactos son la clave, pero ten en cuenta que coroutine y suspended no existen con esos nombres, están ahí solo para que pueda ver la estructura sin tener que entender conceptos más complejos. Veremos todo eso en un minuto.
Funciones de suspensión
Las funciones de suspensión tienen la capacidad de bloquear la ejecución de la corrutina mientras están haciendo su trabajo. Una vez que termina, el resultado de la operación se devuelve y se puede utilizar en la siguiente línea.
val user = suspended { userService.doLogin(username, password) }
val currentFriends = suspended { userService.requestCurrentFriends(user) }
Las funciones de suspensión pueden ejecutarse en el hilo actual o en uno diferente. Depende de cómo se configuren. Las funciones de suspensión solo pueden ejecutarse dentro de una corrutina o dentro de otra función de suspensión.
Para declarar tu propia función de suspensión, solo necesitas usar la palabra reservada suspend:
suspend fun suspendingFunction() : Int {
// Long running task
return 0
}
Volviendo al ejemplo original, una pregunta que puedes estar haciéndote es dónde se ejecuta todo este código. Centrémonos en una sola línea:
¿Dónde crees que se ejecutará esta línea? ¿ Estás seguro de que será en el hilo de UI? Si no lo es, tu aplicación lanzará una excepción, por lo que es una pregunta importante.
Y la respuesta es que depende: depende del contexto de la corrutina.
Contexto de corrutina
El contexto de corrutina es un conjunto de reglas y configuraciones que definen cómo se ejecutará la corrutina. Por debajo, es una especie de Map, con un conjunto de claves y valores posibles.
Por ahora, es suficiente para que sepas que una de las posibles configuraciones es el dispatcher que se utiliza para identificar el hilo donde se ejecutará la corrutina.
Este dispatcher se puede proporcionar de dos maneras:
Explícitamente: Configuramos manualmente el dispatcher que se utilizará.
Por el scope de la corrutina: Olvidemos los scopes por ahora, pero esta sería la segunda opción.
Para hacerlo explicítamente, el builder de corrutinas recibe un contexto de corrutinas como primer parámetro. Así que aquí podemos específicar el dispatcher que se utilizará. Los dispatchers implementan CoroutineContext, por lo que se puede usar aquí:
Ahora, la línea que cambia la visibilidad se ejecuta en el main thread. Ese y todo dentro de esa corrutina. ¿Pero qué pasa con las funciones de suspensión?
coroutine {
...
val user = suspended { userService.doLogin(username, password) }
val currentFriends = suspended { userService.requestCurrentFriends(user) }
...
}
¿Esas solicitudes también se ejecutan en el hilo principal? Si ese es el caso, lo bloquearán, por lo que tendríamos un problema. La respuesta, una vez más, es que depende.
Las funciones de suspensión tienen diferentes formas de definir el dispatcher que se utilizará. Una función muy útil que proporciona la librería de corrutinas es withContext.
withContext
Esta es una función que permite cambiar fácilmente el contexto que se utilizará para ejecutar una parte del código dentro de una corrutina. Es una función de suspensión, lo que significa que suspenderá la corrutina hasta que se ejecute el código interno, sin importar el dispatcher que se use.
Con esto, podemos hacer que nuestras funciones de suspensión usen un hilo diferente:
El código anterior seguiría usando el hilo principal, por lo que bloquearía la interfaz de usuario, pero se puede cambiar fácilmente mediante la especificación de un dispatcher distinto:
Ahora, al utilizar el IO dispatcher, usamos un hilo en background para hacerlo. withContext es una función de suspensión en sí misma, por lo que no necesitamos usarla dentro de otra función de suspensión. En su lugar, podemos hacer:
val user = withContext(Dispatchers.IO) { userService.doLogin(username, password) }
val currentFriends = withContext(Dispatchers.IO) { userService.requestCurrentFriends(user) }
Quizás te preguntes qué dispatchers tenemos y cuándo usarlos. ¡Así que vamos a ver eso ahora!
Dispatchers
Como vimos, los dispatchers son un tipo de contextos de corrutina que especifican el hilo o hilos que pueden ser utilizados por la corrutina para ejecutar su código. Hay dispatchers que solo usan un hilo (como Main) y otros que definen un grupo de hilos que se optimizarán para ejecutar todas las corrutinas que reciben.
Si recuerdas, al principio dijimos que 1 hilo puede ejecutar muchas corrutinas, por lo que el sistema no creará 1 hilo por corrutina, sino que intentará reutilizar los que ya están vivos.
Tenemos cuatro dispatchers principales:
Default: Se usará cuando no se defina un dispatcher, pero también podemos configurarlo explícitamente. Este dispatcher se utiliza para ejecutar tareas que hacen un uso intensivo de la CPU, principalmente cálculos de la propia App, algoritmos, etc. Puede usar tantos subprocesos como cores tenga la CPU. Ya que estas son tareas intensivas, no tiene sentido tener más ejecuciones al mismo tiempo, porque la CPU estará ocupada.
IO: Utiliza este para ejecutar operaciones de entrada/salida. En general, todas las tareas que bloquearán el hilo mientras esperan la respuesta de otro sistema: peticiones al servidor, acceso a la base de datos, sitema de archivos, sensores… ya que no usan la CPU, se puede tener muchas en ejecución al mismo tiempo, por lo que el tamaño de este grupo de hilos es de 64. Las Apps de Android, lo que más hacen, es interactuar con el dispositivo y hacer peticiones de red, por lo que probablemente usarás este la mayoría del tiempo.
Unconfined: Si no te importa mucho qué hilo se utiliza, puedes usar este dispatcher. Es difícil predecir qué hilo se usará, así que no lo uses a menos que estés muy seguro de lo que estás haciendo.
Main: Este es un dispatcher especial que se incluye en las librerías de corrutinas relacionadas con interfaz de usuario. En particular, en Android, utilizará el hilo de UI.
Ahora tienes el poder de controlar los elementos, úsalo sabiamente 🙂
Builders de corrutinas
Ahora que ya sabes cómo cambiar el hilo de ejecución de forma muy sencillas,necesitas aprender cómo iniciar una nueva corrutina. Para ello, utilizarás los coroutine builders.
Tenemos diferentes builders dependiendo de lo que queramos hacer, e incluso técnicamente podrías escribir el tuyo. Pero para la mayoría de los casos, los que proporciona la libreria son más que suficientes. Vamos a verlos:
runBlocking
Este builder bloquea el hilo actual hasta que se terminen todas las tareas dentro de esa corrutina. Esto va en contra de lo que queremos lograr con las corrutinas. Entonces, ¿para qué sirven?
runBlocking es muy útil para implementar tests sobre suspending tasks. En tus tests, envuelve la suspending task que desea probar con una llamada runBlocking y podrás asertar sobre el resultado y evitar que el test finalice antes de que finalice la tarea en segundo plano.
fun testSuspendingFunction() = runBlocking {
val res = suspendingTask1()
assertEquals(0, res)
}
Pero eso es todo. Probablemente no uses runBlocking para mucho más que esto.
launch
Este es el builder más usado. Lo utilizarás mucho porque es la forma más sencilla de crear corrutinas. A diferencia de runBlocking, no bloqueará el subproceso actual (si usamos los dispatchers adecuados, claro).
Este builder siempre necesita un scope. Veremos los scopes en la siguiente sección, pero por ahora fíate de mí y utiliza GlobalScope:
GlobalScope.launch(Dispatchers.Main) {
...
}
launch devuelve un Job, que es otra clase que implementa CoroutineContext.
Los jobs tienen un par de funciones interesantes que pueden ser muy útiles. Pero es importante entender que un Job puede tener a su vez otro Job padre. Ese job padre tiene cierto control sobre los hijos, y ahí es donde entran en juego estas funciones:
job.join
Con está función, puedes bloquear la corrutina asociada con el job hasta que todos los jobs hijos hayan finalizado. Todas las funciones de suspension que se llaman dentro de una corrutina están vinculadas a job, así que el job puede detectar cuándo finalizan todos los jobs hijos y después continuar la ejecución.
val job = GlobalScope.launch(Dispatchers.Main) {
doCoroutineTask()
val res1 = suspendingTask1()
val res2 = suspendingTask2()
process(res1, res2)
}
job.join()
job.join() es una función de suspensión en sí misma, por lo que debe llamarse dentro de otra corrutina.
job.cancel
Esta función cancelará todos sus jobs hijos asociados. Así que, si por ejemplo mientras se está ejecutando suspendingTask1() se llama a cancel(), este no devolverá el valor a res1 y suspendingTask2() no se ejecutará nunca:
val job = GlobalScope.launch(Dispatchers.Main) {
doCoroutineTask()
val res1 = suspendingTask1()
val res2 = suspendingTask2()
process(res1, res2)
}
job.cancel()
job.cancel() esta es una función normal, por lo que no requiere una corrutina para ser llamada.
async
Tenemos este otro builder, que ahora veremos que va a solucionar el segundo problema importante que tuvimos en el ejemplo original.
async permite ejecutar varias tareas en segundo plano en paralelo. No es una función de suspensión en sí misma, por lo que cuando ejecutamos async, el proceso en segundo plano se inicia, pero la siguiente línea se ejecuta de inmediato. async siempre debe llamarse dentro de otra corrutina, y devuelve un job especializado que se llama Deferred.
Este objeto tiene una nueva función llamada await() que es la que bloquea. Llamaremos a await() solo cuando necesitemos el resultado. Si el resultado aún no esta listo, la corrutina se suspende en ese punto. Si ya tenemos el resultado, simplemente lo devolverá y continuará. De esta manera, puedes ejecutar tantas tareas en segundo plano como necesites.
Así que en el siguiente ejemplo, se necesita la primera solicitud para hacer las otras dos. Pero ambas solicitudes de amigos se pueden hacer en paralelo. Usando withContext estamos perdiendo un tiempo precioso:
GlobalScope.launch(Dispatchers.Main) {
val user = withContext(Dispatchers.IO) { userService.doLogin(username, password) }
val currentFriends = withContext(Dispatchers.IO) { userService.requestCurrentFriends(user) }
val suggestedFriends = withContext(Dispatchers.IO) { userService.requestSuggestedFriends(user) }
val finalUser = user.copy(friends = currentFriends + suggestedFriends)
}
Si imaginamos que cada solicitud tarda 2 segundos, el ejemplo anterior tardará 6 segundos (aprox.) en finalizar. Si sustituimos eso con async:
GlobalScope.launch(Dispatchers.Main) {
val user = withContext(Dispatchers.IO) { userService.doLogin(username, password) }
val currentFriends = async(Dispatchers.IO) { userService.requestCurrentFriends(user) }
val suggestedFriends = async(Dispatchers.IO) { userService.requestSuggestedFriends(user) }
val finalUser = user.copy(friends = currentFriends.await() + suggestedFriends.await())
}
La segunda y la tercera tareas se ejecutan en paralelo, por lo que se ejecutarían (idealmente) al mismo tiempo y el tiempo se reduciría a 4 segundos.
Además, sincronizar ambos resultados es trivial. Solo tienes que llamar al await de ambos y dejar que el framework de corrutinas haga el resto.
Scopes
Hasta ahora tenemos un código bastante decente que realiza operaciones complejas de una manera muy simple. Pero todavía tenemos un problema.
Imagina que queremos mostrar la lista de estos amigos en un RecyclerView, pero mientras ejecutamos una de las tareas en segundo plano, el usuario decide cerrar la actividad. La actividad ahora estará en estado isFinishing, por lo que cualquier actualización de la interfaz de usuario lanzará una excepción.
¿Cómo podemos resolver esta situación? Con scopes. Veamos los diferentes ámbitos que tenemos:
Global scope
Es un scope general que se puede usar para cualquier corrutina que deba continuar con la ejecución mientras la aplicación se está ejecutando. Por lo tanto, no deben estar atados a ningún componente específico que pueda ser destruido.
Lo hemos usado antes, así que debería ser fácil ahora:
GlobalScope.launch(Dispatchers.Main) {
...
}
Cuando utilices GlobalScope, siempre pregúntate dos veces si esta corrutina afecta la aplicación completa y no solo una pantalla o componente especifíco.
Implementar CoroutineScope
Cualquier clase puede implementar esta interfaz y convertirse en un scope válido. Lo único que debe hacer es sobrescribir la propiedad coroutineContext.
Aquí, hay al menos dos cosas importantes a configurar: el dispatcher y el job.
Si recuerdas, un contexto puede ser una combinación de otros contextos. Solo tienen que ser de diferente tipo. Así que aquí, en general, definirás dos cosas:
El dispatcher, para identificar el dispatcher por defecto que utilizarán las corrutinas.
El job, para que puedas cancelar todas las corrutinas pendientes en cualquier momento.
class MainActivity : AppCompatActivity(), CoroutineScope {
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job
private lateinit var job: Job
}
La operación plus(+)se utiliza para combinar contextos. Si se concatenan dos contextos de tipo diferente, se creará un CombinedContext que tendrá ambas configuraciones.
Por otro lado, si se concatenan dos del mismo tipo, se utilizará el segundo. Así, por ejemplo: Dispatchers.Main + Dispatchers.IO == Dispatchers.IO
Creamos el job como lateinit para que luego podamos inicializarlo en onCreate. Posteriormente, será cancelado en onDestroy.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
job = Job()
...
}
override fun onDestroy() {
job.cancel()
super.onDestroy()
}
Así que ahora, el código se vuelve más sencillo cuando se utilizan corrutinas. Puedes usar el builder y omitir el contexto corrutina, ya que utilizará el definido por el scope, que incluye el main dispatcher:
launch {
...
}
Por supuesto, si estás utilizando corrutinas en todas tus actividades, puede merecer la pena extraer ese código a una clase padre.
Extra – Convertir callbacks a corrutinas
Si has empezado a pensar en usar corrutinas en tu proyecto, probablemente te estés preguntando cómo vas a seguir utilizando las libreías que usas actualmente, que a lo mejor usan callbacks, en tus nuevas amadas corrutinas.
Para ello, existe una función llamada suspendCancellableCoroutine, que nos permite pasar de un mundo a otro:
suspend fun suspendAsyncLogin(username: String, password: String): User =
suspendCancellableCoroutine { continuation ->
userService.doLoginAsync(username, password) { user ->
continuation.resume(user)
}
}
Esta función devuelve un objeto continuation que se puede utilizar para devolver el resultado del callback. Simplemente llame a continuation.resume y ese resultado será devuelto por la suspending function a la corrutina padre. ¡Es así fácil!
Extra 2 – Sé que me preguntarás por RxJava
Sí, cada vez que menciono las corrutinas, me hacen la misma pregunta: “¿Las corrutinas sustituyen a RxJava? La respuesta corta es no.
La respuesta larga, depende:
Si estás utilizando RxJava solo para pasar del hilo principal a un hilo secundario, ya has visto que las corrutinas pueden hacerlo con bastante facilidad. Así que sí, probablemente no necesites RxJava.
Si haces un uso extensivo de streamms combinándolos, transformándolos, etc, entonces RxJava todavía tiene sentido. Hay una cosa llamada Channels en corrutinas que podría sustituir a los casos más simples de RxJava, pero en general, seguramente prefieras seguir usando los streams de Rx.
Si tienes preguntas más profundas al respecto, entonces probablemente no pueda ayudarte, ya que solo conozco los conceptos básicos de RxJava.
Las corrutinas abren un mundo de posibilidades y simplifican la ejecución de tareas en segundo plano de una manera que probablemente no podrías imaginar.
Realmente te recomiendo que comiences a usarlas en tus proyectos.
¡Disfruta escribiendo tus propias corrutinas!
¡Y recuerda! Si deseas aprender Kotlin fácilmente de una manera estructurada y en muy poco tiepo, no puedo dejar de recomendarte que consultes mi training online certificado por JetBrains. Y por un tiempo muy limitado, consíguelo con un descuento del 40%. ¡Tal vez puedas convencer a tu jefe! Al final el curso, recibirás un certificado de finalización.
Utilizamos cookies propias y de terceros para analizar nuestros servicios y mostrarte publicidad relacionadas con tus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación (por ejemplo, páginas visitadas).
Funcional Siempre activo
El almacenamiento o acceso técnico es estrictamente necesario para el propósito legítimo de permitir el uso de un servicio específico explícitamente solicitado por el abonado o usuario, o con el único propósito de llevar a cabo la transmisión de una comunicación a través de una red de comunicaciones electrónicas.
Preferencias
El almacenamiento o acceso técnico es necesario para la finalidad legítima de almacenar preferencias no solicitadas por el abonado o usuario.
Estadísticas
El almacenamiento o acceso técnico que es utilizado exclusivamente con fines estadísticos. El almacenamiento o acceso técnico que se utiliza exclusivamente con fines estadísticos anónimos. Sin un requerimiento, el cumplimiento voluntario por parte de tu Proveedor de servicios de Internet, o los registros adicionales de un tercero, la información almacenada o recuperada sólo para este propósito no se puede utilizar para identificarte.
Marketing
El almacenamiento o acceso técnico es necesario para crear perfiles de usuario para enviar publicidad, o para rastrear al usuario en una web o en varias web con fines de marketing similares.