Tabla de Contenidos
Corrutinas Kotlin en Android
Os dejo un vídeo explicativo de resumen y lo amplío en la descripción a continuación. Al final en el apartado de referencias del apartado de corrutinas tenéis más información para ampliar y complementar lo aquí expuesto.
- Corrutinas Kotlin en 5 minutos, aplicadas en Android Conceptos que se tratan en el vídeo son (ampliados):
Una de las grandes ventajas para el programador al hacer uso de corrutinas y las funciones suspend es que la lógica o el flujo de nuestro programa se asemeja a una programación lineal, donde una linea llama a una función y obtiene un resultado, la linea siguiente usa ese resultado, pero el resultado se ha obtenido en una operación asíncrona.
De esta forma no tenemos que definir funciones callback tan habituales en la programación con Android Views. Ahora con Compose y Corrutinas, aunque inicialmente es mas lío si conoces el funcionamiento anterior, todo se simplifica y finalmente es más intuitivo.
📦Coroutine Builders
Builder | Descripción |
---|---|
runBlocking | Bloquea el hilo actual hasta que termina la corrutina. Útil solo para tests y main en programas kotlin. |
launch | Lanza una corrutina que no devuelve resultado pero puede retornar el handler de un Job . |
async | Lanza una corrutina que devuelve un resultado futuro (Deferred<T> ). Se espera con await() . |
🧭 Scopes: ¿Dónde lanzamos las corrutinas?
Un scope define el contexto y ciclo de vida de una corrutina. Según el componente Android, usaremos uno distinto:
Componente Android | Scope recomendado | Motivo |
---|---|---|
Application | GlobalScope | Vive toda la vida de la app (ojo con memory leaks). |
Activity / Fragment | lifecycleScope | Atado al ciclo de vida de la Activity. |
ViewModel | viewModelScope | Vive mientras viva el ViewModel. |
Ejemplo (en Activity):
lifecycleScope.launch { val result = withContext(Dispatchers.IO) { DataProvider.doHeavyTask() } println(result) }
⚙️ Dispatchers: ¿En qué hilo se ejecuta?
Los dispatchers deciden en qué tipo de hilo se ejecuta la corrutina:
Dispatcher | Uso principal |
---|---|
Dispatchers.Main | UI: actualizar vistas, interactuar con el usuario. |
Dispatchers.IO | Entrada/Salida: archivos, red, base de datos. |
Dispatchers.Default | Tareas intensivas en CPU (sorting, JSON parsing, etc). |
Regla general en Android
- La corrutina se lanza en
Dispatchers.Main
(por defecto en lifecycleScope/viewModelScope). - Las funciones de suspensión pesadas deben ejecutarse en
Dispatchers.IO
oDefault
usandowithContext
.
Ejemplo:
viewModelScope.launch { val datos = withContext(Dispatchers.IO) { obtenerDatosDesdeInternet() } actualizarUI(datos) // Aquí sigue en el hilo principal }
🛠️ Controlando múltiples tareas
🔁 Concurrente (con async
)
val resultado1 = async { tareaLenta1() } val resultado2 = async { tareaLenta2() } val suma = resultado1.await() + resultado2.await()
async
devuelve unDeferred
.await()
suspende la corrutina hasta que el resultado esté listo.- Si se ejecutan en
Dispatchers.Default
oIO
, pueden correr en paralelo si hay núcleos disponibles.
Paralelismo
Dos hilos pueden estar activos, pero eso no significa que se estén ejecutando simultáneamente.
🔸 Por ejemplo:
- En un sistema con 1 solo núcleo (CPU), solo 1 hilo puede ejecutarse a la vez.
- El sistema operativo intercambia entre hilos rápidamente (context switching), dando la ilusión de que están trabajando al mismo tiempo (concurrencia), pero no es paralelismo real.
🔍 Ejecución real de hilos
🔹 ¿Qué pasa cuando lanzas varios hilos?
- Si tienes 4 hilos y 1 núcleo → se ejecutan concurrentemente (el sistema los intercambia).
- Si tienes 4 hilos y 4 núcleos → pueden ejecutarse realmente en paralelo.
- Si tienes 10 hilos y 4 núcleos → hay paralelismo parcial (4 a la vez), el resto espera (concurrencia).
fun main() = runBlocking { repeat(10) { i -> launch(Dispatchers.Default) { println("Comienza tarea $i en hilo ${Thread.currentThread().name}") delay(1000) println("Termina tarea $i") } } }
Dispatchers.Default
usa un pool de hilos igual al número de núcleos.- Si tu máquina tiene 4 núcleos, verás que 4 tareas se ejecutan en paralelo, el resto se intercalan.
- Todos los hilos son distintos, pero no todos se ejecutan al mismo tiempo.
🧠 ¿Se ejecutan en núcleos distintos?
Depende del dispatcher usado:
Dispatcher | Qué hace |
---|---|
Dispatchers.Default | Usa un pool de hilos múltiples, puede usar varios núcleos. ✅ Paralelo |
Dispatchers.IO | Para tareas de entrada/salida (acceso a red, ficheros). ✅ Paralelo |
Dispatchers.Main | Solo un hilo (el de UI). ❌ No paralelo |
Sin especificar | Hereda el dispatcher del coroutineScope padre |
🔍 Si estás en runBlocking
sin dispatcher, por defecto usas un solo hilo. Si usas Dispatchers.Default
, hay paralelismo real (si hay nucleos suficientes)
Cuidado: async
≠ automático en hilos distintos
val a = async { ... } // No garantiza que sea en otro hilo
- Si estás en
Dispatchers.Main
, se ejecuta en el mismo hilo que tú. - Para asegurarte de que es paralelo, debes usar
withContext(Dispatchers.Default)
oasync(Dispatchers.Default)
.
Ejemplo paralelo real
runBlocking { val tiempo = measureTimeMillis { val a = async(Dispatchers.Default) { delay(1000); 10 } val b = async(Dispatchers.Default) { delay(1000); 20 } println("Resultado: ${a.await() + b.await()}") } println("Tiempo: $tiempo ms") }
⏱️ Esto se ejecutará en ~1000 ms (porque las dos corrutinas corren en paralelo).
📌 Resumen
Pregunta | Respuesta |
---|---|
¿async ejecuta en otro hilo automáticamente? | ❌ No necesariamente. Depende del Dispatcher . |
¿Es concurrente? | ✅ Siempre. Se ejecutan en paralelo lógico. |
¿Es paralelo? | ✅ Solo si usas un Dispatcher con múltiples hilos (Default , IO , etc.). |
¿await() bloquea? | ❌ No bloquea el hilo. Solo suspende hasta que el resultado esté disponible. |
🧩 Ejemplo completo con withContext
, async
, launch
viewModelScope.launch { val datos = withContext(Dispatchers.IO) { descargarDatos() } val procesado = async(Dispatchers.Default) { procesarDatos(datos) } actualizarUI(procesado.await()) }
🧬 Ciclo de vida: Job
, join()
, cancel()
Cada corrutina lanzada con launch
o async
devuelve un objeto Job
.
Métodos clave:
Método | Qué hace |
---|---|
job.join() | Suspende hasta que ese Job termine (función de suspensión). |
job.cancel() | Cancela el job y todas sus tareas hijas. |
val job = viewModelScope.launch { val res1 = suspendingTask1() val res2 = suspendingTask2() println("Resultado: ${res1 + res2}") } job.cancel() // cancela todo si aún está en ejecución</code>
🧪 ¿Y qué pasa con runBlocking
?
fun main() { runBlocking { val datos = obtenerDatos() println(datos) } }
- Solo se usa en tests o funciones
main
de consola. - Bloquea el hilo actual (al contrario que
launch
oasync
). - Útil para probar funciones
suspend
.
✅ Buenas prácticas en Android
- No uses
GlobalScope
salvo casos muy justificados. - Usa
lifecycleScope
yviewModelScope
para evitar fugas de memoria. - Ejecuta tareas pesadas con
withContext(Dispatchers.IO)
oDefault
. - Usa
launch
para tareas sin retorno yasync
cuando necesites un valor. - Recuerda:
async
lanza concurrentemente, pero el paralelismo depende del dispatcher y núcleos disponibles.
- Tenemos varios Scopes, Application, Activity, ViewModel, … es importante diferenciar en qué ámbito definimos/lanzamos la corrutina.
- Cada Scope tiene un dispatcher por defecto.
El Dispatcher.default es el por defecto para el GlobalScope.
Este dispatcher coloca la corrutina en un hilo diferente del hilo principal.
En Kotlin, todas las corrutinas se deben ejecutar en un despachador, incluso cuando se ejecutan en el subproceso principal.
La corrutinas se pueden suspender a sí mismas, y el despachador es responsable de reanudarlas.
Para especificar en qué lugar deberían ejecutarse las corrutinas, Kotlin proporciona tres despachadores que puedes utilizar:- Dispatcher.Main: Utiliza este despachador para ejecutar una corrutina en el Main Thread de Android.
Solo debes usar este despachador para interactuar con la IU y realizar trabajos rápidos. - Dispatcher.Default: Este despachador está optimizado para realizar trabajo que usa la CPU de manera intensiva fuera del Main Thread. 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. Algunos casos prácticos de ejemplo son clasificar una lista y analizar JSON.
- Dispatcher.IO: Este despachador está optimizado para realizar E/S de disco o red fuera del subproceso principal. ya que no usan la CPU, se puede tener muchas en ejecución al mismo tiempo. 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. Algunos ejemplos incluyen usar el componente Room, leer desde archivos o escribir en ellos, y ejecutar operaciones de red.
- Dispatcher.Main: Utiliza este despachador para ejecutar una corrutina en el Main Thread de Android.
- Lo que querremos en Android es que la corrutina se ejecute en el hilo principal, pero que las funciones de suspensión que se lancen desde la corrutina se ejecuten en hilos separados.
Esto es así para que la corrutina tenga acceso a todos los componentes del hilo principal y pueda actualizar datos de éstos con los resultados que vienen de las funciones de suspensión. - Con withContext(Dispatcher){código} podemos decidir que parte de código dentro de una corrutina se ejecuta en el scope asociado al dispatcher que se pasa como parámetro.
withContext() devuelve el retorno que tenga la función o código dentro de el, bloqueando la corrutina hasta que obtiene el resultado, pero liberando el hilo principal.
Veamos el ejemplo (usado en Application):
GlobalScope.launch(Dispatcher.Main){ //this:CoroutineScope val result = withContext(Dispatcher.IO){ DataProvider.DoHeavyTask() } println(result) }
- Cuando queremos realizar algo a nivel de Activity necesitamos un Scope a nivel de Activity para que cuando muera la activity, el proceso muera también.
El objeto CoroutineScope realiza un seguimiento de cualquier corrutina que crea mediante los elementos launch o async.
Android proporcina para la Activity el lifecycleScope y para el ViewModel el viewModelScope
Veamos el ejemplo (usado en Activity y en ViewModel): (No hace falta indicar el Dispatcher.Main en el launch porque por defecto usan ese.
//Para usar en una clase Activity lifecycleScope.launch{ //this:CoroutineScope val result = withContext(Dispatcher.IO){ DataProvider.DoHeavyTask() } println(result) } //Para usar en una clase ViewModel viewModelScope.launch{ //this:CoroutineScope val result = withContext(Dispatcher.IO){ DataProvider.DoHeavyTask() } println(result) }
- Builders:
- 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é sirve?
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.
O para probar como hemos hecho anteriormente en el proyecto Kotlin.
No se usará habitualmente.
- 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.
launch devuelve un Job, que es otra clase que implementa CoroutineContext.
Los jobs tienen un par de funciones interesantes que pueden ser muy útiles, las vemos más abajo.
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 (Join y Cancel).
- async: 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 Deferred 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.
- runBlocking: Este builder bloquea el hilo actual hasta que se terminen todas las tareas dentro de esa corrutina.
Jobs y funciones
- El elemento Job es un controlador de corrutinas.
Cada corrutina que creas con launch o async muestra una instancia de Job que identifica de forma única la corrutina y administra su ciclo de vida. - 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 suspensión 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.
job.join() es una función de suspensión en sí misma, por lo que debe llamarse dentro de otra corrutina.
val job = GlobalScope.launch(Dispatchers.Main) { doCoroutineTask() val res1 = suspendingTask1() val res2 = suspendingTask2() process(res1, res2) } job.join()
- 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. Recordar que como no se ha usado async las suspendingTasks del ejemplo van en secuencial.
job.cancel() esta es una función normal, por lo que no requiere una corrutina para ser llamada.
Algunas referencias
- Corrutinas de Kotlin en Android
- Cómo mejorar el rendimiento de una app con corrutinas de Kotlin
- Coroutines: first things first En este artículo, se enseñan conceptos básicos de corrutinas, incluidos los elementos CoroutineScope, Job y CoroutineContext.
- The ABC of Coroutines – Kotlin Vocabulary Obtén información sobre las clases y funciones más comunes que se usan para trabajar con corrutinas.
- Coroutines on Android (part I): Getting the background Esta publicación es la primera de una serie en la que se enseña sobre las corrutinas de Kotlin.
- Understand Kotlin Coroutines on Android (Google I/O’19) En esta charla de Google I/O 2019, se brinda una descripción general del uso de las corrutinas de Kotlin en Android.
- Codelab de corrutinas (avanzado los requisitos todavía no se han visto pero podéis echarle un vistazo):
En este codelab, se muestra cómo usar corrutinas de Kotlin para administrar subprocesos en segundo plano y simplificar tu código asíncrono. - Android Coroutines: How to manage async tasks in Kotlin: Obtén información sobre el estado de las corrutinas en Android desde el año 2020.
- REPASO sobre las CORRUTINAS en ANDROID: Con ejemplo de una aplicación con Login.