launch/join vs async/await en Kotlin: ¿qué elegir en 2025? Análisis completo con ejemplos
\n\n¡Hola, colega! Si alguna vez has escrito corrutinas en Kotlin, seguramente te has preguntado: «¿Cuál es realmente la diferencia entre launch y async? ¿Y para qué sirve ese misterioso join?». Esta pregunta es un verdadero éxito en Stack Overflow, con miles de visitas. Y no es casualidad: a primera vista, ambas herramientas hacen lo mismo: ejecutar una tarea en segundo plano. Pero el diablo, como siempre, está en los detalles.
En 2025, cuando Kotlin coroutines se han convertido en algo común en el desarrollo de Android y backend, comprender estas diferencias no es solo un capricho, sino una necesidad para escribir código limpio, seguro y eficiente. En este artículo no solo enumeraremos datos secos. Analizaremos escenarios reales, veremos el código y extraeremos las mejores prácticas que puedes aplicar con confianza en tus proyectos ahora mismo.
\n\n¡Allá vamos!
\n\nlaunch: «Ejecuta y olvida»
\n\nLa función launch es, probablemente, la forma más sencilla de iniciar una corrutina. Crea una nueva corrutina que no devuelve un resultado. Su tarea es realizar algún trabajo: actualizar la UI, enviar un registro, escribir datos en un archivo. El tipo de valor devuelto es Job. Es como darle una tarea a un mensajero: «Lleva este paquete y vuelve, no necesito un informe».
El método join permite esperar a que esta corrutina termine. Suspende la ejecución de la corrutina actual hasta que el job que llamó a join finalice.
Veamos un ejemplo. Supongamos que necesitamos cargar dos imágenes en paralelo y luego combinarlas. Con launch y join se vería así:
import kotlinx.coroutines.*\n\nsuspend fun loadImage(url: String): Image {\n // simulación de carga\n delay(1000)\n return Image(url)\n}\n\ndata class Image(val url: String)\n\nsuspend fun loadTwoImagesWithLaunch() = coroutineScope {\n var image1: Image? = null\n var image2: Image? = null\n\n val job1 = launch {\n image1 = loadImage("https://example.com/img1.png")\n }\n\n val job2 = launch {\n image2 = loadImage("https://example.com/img2.png")\n }\n\n // Esperamos ambas tareas\n job1.join()\n job2.join()\n\n // Ahora image1 e image2 están cargadas garantizadamente\n println("Images loaded: $image1 and $image2")\n}\n\n¿Qué es importante aquí? Usamos variables mutables (var) para guardar el resultado. Esto no es muy elegante y puede provocar problemas con hilos, aunque las corrutinas trabajen dentro de un mismo hilo. Pero la principal desventaja es que no podemos usar launch para devolver un valor directamente.
async: «Ejecuta y obtén el resultado»
\n\nEn cambio, async es como «clase business». Crea una corrutina que devuelve un resultado en forma de un objeto Deferred<T>. Es como pedir un taxi: esperas y no solo obtienes el hecho del viaje, sino un resultado concreto: llegaste a tu destino. Para obtener este resultado, se usa await().
El mismo ejemplo de carga de imágenes, pero usando async/await, se vuelve más elegante:
import kotlinx.coroutines.*\n\nsuspend fun loadImage(url: String): Image {\n delay(1000)\n return Image(url)\n}\n\ndata class Image(val url: String)\n\nsuspend fun loadTwoImagesWithAsync() = coroutineScope {\n val deferred1 = async { loadImage("https://example.com/img1.png") }\n val deferred2 = async { loadImage("https://example.com/img2.png") }\n\n // await() suspende la corrutina hasta que obtiene el resultado\n val image1 = deferred1.await()\n val image2 = deferred2.await()\n\n println("Images loaded: $image1 and $image2")\n}\n\n¿Notas la diferencia? Sin var, sin gestión manual. Todo limpio,