Si alguna vez has sentido que tu aplicación de Android se queda «colgada» o que la interfaz no responde mientras descargas algo de la red, es probable que estés peleando con el hilo principal. En el mundillo del desarrollo móvil, conseguir que la app sea fluida mientras se hacen tareas pesadas es el santo grial, y aquí es donde entran en juego las corrutinas de Kotlin, que básicamente nos permiten escribir código asíncrono sin volvernos locos con callbacks infinitos.
Imagina que las corrutinas son como hilos ligeros; no crean un hilo nuevo del sistema operativo cada vez, sino que aprovechan grupos de hilos ya existentes y un sistema de planificación muy inteligente. Esto hace que sean increíblemente eficientes y que el código se lea casi como si fuera secuencial, aunque por debajo esté ocurriendo toda una danza de suspensiones y reanudaciones para que el usuario no note ningún tirón en la pantalla.
El secreto de la suspensión: suspend y resume
A diferencia de las funciones normales, las corrutinas introducen dos conceptos clave. Por un lado tenemos suspend, que permite pausar la ejecución de una tarea guardando el estado actual, y por otro resume, que retoma la marcha justo donde se quedó. Lo curioso es que una función suspendida no bloquea el hilo donde se ejecuta; simplemente se aparta para dejar que otras tareas pasen mientras espera una respuesta, por ejemplo, de una base de datos.
Es fundamental entender que poner la palabra suspend no mueve automáticamente el código a un hilo secundario. Para lograr esa seguridad del hilo principal (main-safety), debemos combinar estas funciones con los despachadores adecuados, asegurando que cualquier operación costosa no asfixie la interfaz de usuario.
Dispatchers: ¿Dónde ocurre la magia?
Para que Kotlin sepa en qué hilo debe ejecutar cada trozo de código, utilizamos los Dispatchers. No todos sirven para lo mismo, así que hay que saber elegir:
- Dispatchers.Main: Es el encargado de tocar la interfaz de usuario. Solo debe usarse para cambios visuales rápidos o interactuar con LiveData, ya que si haces algo pesado aquí, te ganarás un error de Application Not Responding (ANR).
- Dispatchers.IO: Está optimizado para tareas de entrada y salida. Es el sitio ideal para llamadas a APIs, leer archivos del disco o gestionar la base de datos Room.
- Dispatchers.Default: Diseñado para el trabajo intensivo de CPU, como cuando tienes que procesar un JSON gigante o ordenar una lista enorme de elementos.
Una técnica muy común y recomendada es usar withContext. Esta función nos permite cambiar el despachador dentro de una función suspendida sin necesidad de callbacks. De este modo, el desarrollador que llama a la función no tiene que preocuparse de en qué hilo está, ya que la propia función garantiza su propia seguridad al cambiar internamente al hilo IO o Default.
Launch frente a Async: ¿Cuál elegir?
Cuando queremos arrancar una corrutina, tenemos dos caminos principales dependiendo de si necesitamos que la tarea nos devuelva algo o no.
El operador launch es lo que llamamos «dispara y olvida». Se utiliza para tareas que no devuelven un resultado, como actualizar un color en la pantalla o guardar un dato en caché. Retorna un objeto Job, que es básicamente un mando a distancia que nos permite cancelar la corrutina si ya no nos hace falta.
Por otro lado, async se utiliza cuando sí necesitamos un valor de vuelta. Este operador devuelve un Deferred<T>, que es como una promesa de que habrá un resultado en el futuro. Para obtener dicho valor, tenemos que llamar a la función await(), la cual suspenderá la ejecución hasta que el resultado esté listo. Un punto fuerte de async es que permite realizar llamadas en paralelo; si lanzamos tres async, las tres peticiones irán al servidor a la vez, optimizando el tiempo de espera total.
Concurrencia estructurada y gestión del ciclo de vida
Para evitar que las corrutinas sigan ejecutándose cuando el usuario ya ha cerrado la pantalla (lo que causaría fugas de memoria), Kotlin utiliza la concurrencia estructurada. Todo se organiza mediante el CoroutineScope, que rastrea todas las corrutinas lanzadas en su interior.
En Android, lo más habitual es usar scopes ya predefinidos como viewModelScope o lifecycleScope. Estos son maravillosos porque se encargan de cancelar automáticamente todo el trabajo pendiente cuando el ViewModel se destruye o la Activity finaliza. Si necesitas crear tu propio scope, puedes combinar un Job y un Dispatcher, pero recuerda siempre llamar a scope.cancel() en el método de limpieza de tu clase.
Manejo de errores y casos especiales
La gestión de excepciones varía según el constructor que uses. En launch, si ocurre un error y no está capturado, la aplicación se cerrará inmediatamente. Sin embargo, en async, la excepción se guarda dentro del objeto Deferred y no saltará hasta que llamemos a await(), lo que a veces puede hacer que los errores pasen desapercibidos si no tenemos cuidado.
Para manejar esto de forma natural, lo mejor es envolver las llamadas en bloques try-catch, lo que hace que el código asíncrono se sienta tan sencillo como el síncrono. Además, si queremos que una corrutina sea inmune a la cancelación del padre, podemos usar NonCancellable con withContext, aunque esto debe hacerse con mucha cautela para no romper la jerarquía de la concurrencia.
Integrar todo esto con librerías como Retrofit es pan comido, ya que las versiones modernas soportan funciones suspendidas de forma nativa. Al final, el flujo ideal es que el ViewModel lance la corrutina, el Repositorio gestione la lógica de negocio y el Data Source realice la petición en Dispatchers.IO, manteniendo siempre la interfaz fluida y el código limpio.
El uso de corrutinas transforma la manera de desarrollar en Android, sustituyendo la complejidad de los hilos tradicionales por un sistema de suspensión eficiente y scopes ligados al ciclo de vida. Al dominar la diferencia entre el disparo rápido de launch y la potencia paralela de async con await, junto a una correcta elección de Dispatchers, se logra un software robusto que aprovecha al máximo los recursos del dispositivo sin comprometer la experiencia del usuario. Comparte la guía y otros usuarios sabrán del tema.
Continúar leyendo...