Uso correcto de los Dispatchers: Main, IO y Default

  • Los Dispatchers determinan el hilo de ejecuciĂ³n, optimizando tareas segĂºn sean de CPU, E/S o interfaz de usuario.
  • El uso de withContext permite cambiar el contexto de ejecuciĂ³n de forma segura sin bloquear el hilo principal.
  • La concurrencia estructurada mediante Scopes y Jobs garantiza que las tareas se cancelen correctamente evitando fugas de memoria.

Uso correcto de los Dispatchers Main, IO y Default

Si alguna vez has sentido que tu aplicaciĂ³n de Android se queda congelada al cargar unos datos, es muy probable que estĂ©s peleando con el hilo principal. Las corrutinas de Kotlin llegaron para salvarme la vida a muchos desarrolladores, permitiĂ©ndonos escribir cĂ³digo asĂ­ncrono que parece secuencial y lineal, eliminando esos callbacks infinitos que hacĂ­an que el cĂ³digo pareciera una escalera.

Para aprovechar este superpoder, no basta con poner la palabra suspend delante de una funciĂ³n. Lo realmente importante es saber dĂ³nde se ejecuta cada trozo de cĂ³digo, y ahĂ­ es donde entran en juego los Dispatchers, que actĂºan como los directores de orquesta que deciden quĂ© hilo se encarga de cada tarea.

¿QuĂ© demonios es un Dispatcher?

BĂ¡sicamente, un Dispatcher es el encargado de asignar una corrutina a un hilo o grupo de hilos especĂ­ficos. Debemos recordar que, aunque las corrutinas son ligeras y no bloquean, por debajo siguen existiendo hilos que las soportan. Configurar el dispatcher adecuado evita que la app se cierre o que el rendimiento caiga en picado.

Cuando lanzamos una corrutina con launch, normalmente hereda el contexto donde se creĂ³. Sin embargo, si necesitamos que una parte especĂ­fica de la lĂ³gica se mueva a otro hilo, podemos indicarlo explĂ­citamente. Para comprobar en quĂ© hilo estamos realmente, un truco muy Ăºtil es imprimir Thread.currentThread().name, lo que nos permite ver la magia ocurriendo en tiempo real.

El trĂ­o dinĂ¡mico: Main, IO y Default

Kotlin nos ofrece varias opciones, pero hay tres que vas a usar el 99% del tiempo. Primero tenemos Dispatchers.Main, que es el hilo de la interfaz de usuario. Solo debe usarse para cosas rapidĂ­simas, como actualizar un texto en pantalla o interactuar con componentes de la UI. Si te pones a hacer cĂ¡lculos aquĂ­, la app se colgarĂ¡ y el usuario se desesperarĂ¡.

Luego tenemos Dispatchers.IO, que es el caballo de batalla para todo lo que sea entrada y salida de datos. Es ideal para leer archivos, hacer peticiones a una API o consultar la base de datos con Room. Este dispatcher usa un pool de hilos mucho mĂ¡s amplio porque las tareas de E/S suelen pasar mucho tiempo esperando una respuesta externa sin consumir CPU.

Por Ăºltimo, encontramos Dispatchers.Default. Este estĂ¡ optimizado para tareas que exprimen el procesador, es decir, trabajo CPU-bound. Si tienes que analizar un JSON gigante, ordenar una lista enorme o hacer cĂ¡lculos matemĂ¡ticos complejos, este es tu sitio. Sus hilos suelen coincidir con la cantidad de nĂºcleos de la CPU para no saturar el sistema.

También existe Dispatchers.Unconfined, pero la verdad es que apenas se utiliza en proyectos reales, ya que comienza donde se llama y puede reanudarse en cualquier hilo, lo que lo hace bastante impredecible para la mayoría de los casos.

El arte de cambiar de hilo con withContext

AquĂ­ es donde ocurre la magia de la seguridad del hilo principal (main-safety). La funciĂ³n withContext permite suspender la corrutina actual, saltar a otro dispatcher, ejecutar el bloque de cĂ³digo y, una vez terminado, volver automĂ¡ticamente al dispatcher original.

Lo mejor de este patrĂ³n es que el emisor de la funciĂ³n no tiene que preocuparse de en quĂ© hilo llamarla. Una buena prĂ¡ctica es que la propia funciĂ³n suspendida gestione su propio contexto. Por ejemplo, una funciĂ³n getDatos() deberĂ­a llamar internamente a withContext(Dispatchers.IO), asĂ­ el ViewModel puede llamarla desde el hilo principal sin miedo a bloquear la pantalla.

En cuanto al rendimiento, withContext es extremadamente eficiente. No añade una carga extra comparado con los callbacks y, en ocasiones, Kotlin es capaz de optimizar el intercambio de hilos si detecta que ya estamos en el dispatcher deseado, evitando saltos innecesarios.

Controlando el ciclo de vida: Scopes y Jobs

Uso correcto de los Dispatchers: Main, IO y Default

Lanzar corrutinas al azar es una receta para el desastre y las fugas de memoria. Para evitarlo, usamos los CoroutineScopes, que definen el tiempo de vida de la tarea. En Android, tenemos viewModelScope para los ViewModels y lifecycleScope para las Activities o Fragments; cuando el componente muere, las corrutinas se cancelan automĂ¡ticamente.

Cada vez que usamos launch o async, se genera un objeto Job. Este Job es como un ticket que nos permite controlar la corrutina: podemos usar job.join() para esperar a que termine o job.cancel() para detenerla si el resultado ya no nos interesa. Esto es la base de la concurrencia estructurada: los hijos estĂ¡n ligados al padre y no quedan procesos huĂ©rfanos dando vueltas por la memoria.

EjecuciĂ³n paralela y la diferencia entre launch y async

A veces queremos hacer varias cosas a la vez para ganar tiempo. Para ello usamos async, que devuelve un objeto Deferred. A diferencia de launch (que es bĂ¡sicamente un dispara y olvida), async nos permite recuperar un valor mediante la funciĂ³n await().

Ojo aquĂ­: que uses async no significa que automĂ¡ticamente haya paralelismo. Si lanzas dos async en Dispatchers.Main, se ejecutarĂ¡n uno tras otro en el mismo hilo. Para lograr un paralelismo real, debes asignarles Dispatchers.Default o IO, permitiendo que se distribuyan en los distintos nĂºcleos de la CPU.

Para gestionar grupos de tareas paralelas, el constructor coroutineScope es fundamental. Este asegura que la funciĂ³n no termine hasta que todas las corrutinas hijas hayan completado su trabajo, capturando ademĂ¡s cualquier excepciĂ³n que pueda surgir en el camino para que no se pierda en el limbo.

Para cerrar el cĂ­rculo, es fundamental no abusar de GlobalScope ya que es muy difĂ­cil de testear y cancelar. Lo ideal es siempre apoyarse en la jerarquĂ­a de scopes de Android y usar el dispatcher correcto segĂºn la carga de trabajo. Al combinar viewModelScope, withContext para las tareas pesadas y async para la concurrencia, conseguimos aplicaciones robustas que mantienen la interfaz fluida mientras procesan datos en segundo plano sin despeinarse.


Add as preferred source