Noticia Cómo suspender funciones de librerías antiguas usando suspendCancellableCoroutine

Cómo suspender funciones de librerías antiguas usando suspendCancellableCoroutine


Seguro que te ha pasado alguna vez: te toca trabajar con una librería de hace años que usa el típico sistema de callbacks y sientes que el código se vuelve un caos. Esa estructura, donde una función llama a otra y esta a su vez a otra, acaba creando el famoso «callback hell», haciendo que mantener el proyecto sea una auténtica pesadilla y que leer el flujo de datos sea como intentar descifrar un jeroglífico.

Afortunadamente, Kotlin nos ofrece las corrutinas, que básicamente son como hilos pero mucho más ligeros y eficientes. Gracias a ellas, podemos transformar esas llamadas asíncronas antiguas en un estilo de programación secuencial, logrando que el código sea mucho más limpio, legible y, sobre todo, más fácil de depurar sin bloquear el hilo principal de la aplicación.

El problema de los callbacks y la llegada de las corrutinas​


Cuando usamos APIs basadas en devoluciones de llamada, la lógica se fragmenta. Imagina que tienes que validar un usuario, luego pedir sus amigos y finalmente sugerirle otros nuevos; si cada paso depende de un callback, terminarás con una indentación infinita hacia la derecha. Además, gestionar los errores en cada nivel de esa pirámide es agotador y propenso a fallos.

Aquí es donde entran las funciones de suspensión. Estas permiten que una corrutina se detenga en un punto concreto y devuelva el control al sistema hasta que el resultado esté listo. Lo mejor de todo es que, para quien lee el código, parece que la operación es síncrona, aunque por debajo el hilo no esté bloqueado y la aplicación siga respondiendo con fluidez.

Dominando suspendCancellableCoroutine​


Para cerrar la brecha entre el mundo de los callbacks y el de las corrutinas, disponemos de una herramienta fundamental llamada suspendCancellableCoroutine. Esta función actúa como un puente que nos proporciona un objeto CancellableContinuation, el cual nos permite reanudar la ejecución de la corrutina manualmente una vez que la API antigua nos devuelva el dato esperado.

A diferencia de su versión simplificada, suspendCoroutine, la variante cancellable es la opción preferida en el desarrollo profesional. ¿La razón? Nos permite gestionar la cancelación del Job. Si el usuario cierra la pantalla mientras la petición sigue en curso, podemos evitar que la corrutina se reanude innecesariamente, previniendo así fugas de memoria y consumo de recursos.

Para implementar esto, simplemente envolvemos la llamada asíncrona y, dentro del callback de éxito, llamamos a continuation.resume(resultado). Si la operación falla, utilizamos continuation.resumeWithException(e) para que la excepción suba hasta el punto de llamada y pueda ser capturada con un bloque try-catch convencional, eliminando la necesidad de manejar errores en múltiples callbacks.

Gestión avanzada de la cancelación y recursos​


Uno de los puntos más críticos es asegurar que no dejemos procesos colgados. El método invokeOnCancellation es vital aquí, ya que nos permite instalar un manejador que se ejecutará siempre que la corrutina sea cancelada. Esto es fundamental si la API antigua requiere que desregistremos manualmente un listener o cerremos un archivo para no dejar basura en el sistema.

Es importante entender que la cancelación en Kotlin es generalmente asíncrona. La garantía de cancelación inmediata asegura que, si el Job fue cancelado mientras la función estaba suspendida, la corrutina no se reanudará con éxito, incluso si el método de reanudación ya fue invocado. Esto protege la integridad de la aplicación evitando que se ejecute código en componentes de UI ya destruidos.

Cuando un solo resultado no basta: callbackFlow​


Hay casos donde no buscamos un único valor, sino un flujo continuo de datos, como las actualizaciones de la ubicación GPS. Para esto, suspendCancellableCoroutine se queda corto y debemos recurrir a callbackFlow. Este constructor nos permite emitir múltiples valores a través de un flujo asíncrono basado en canales.

Dentro de un callbackFlow, utilizamos la función offer (o trySend en versiones recientes) para enviar datos al flujo conforme llegan. Un detalle obligatorio es la llamada a awaitClose al final del bloque; este es el lugar donde debemos limpiar los recursos y desregistrar los callbacks para que el flujo no quede abierto eternamente consumiendo batería y CPU.

Para optimizar estos flujos, podemos usar operadores como conflate(), que es ideal cuando solo nos interesa la actualización más reciente y queremos ignorar los valores intermedios si el recolector es más lento que el emisor. Además, integrar esto con asLiveData() permite que la suscripción se active y desactive automáticamente según el ciclo de vida de la Activity.

Contextos, Dispatchers y optimización​


Para que todo esto funcione sin congelar la pantalla, debemos gestionar el contexto de la corrutina. El Dispatchers.Main se encarga de la interfaz de usuario, pero para las llamadas a librerías antiguas que hacen I/O, debemos usar Dispatchers.IO. La función withContext es la herramienta perfecta para cambiar el hilo de ejecución de forma puntual sin romper la estructura secuencial.

Si necesitamos ejecutar varias tareas en paralelo para ganar tiempo, el builder async es la clave. En lugar de esperar a que una termine para empezar la otra, lanzamos ambas y utilizamos await() para sincronizar los resultados justo cuando los necesitamos. Esto puede reducir drásticamente el tiempo de carga de una pantalla, pasando de una ejecución lineal a una concurrente.

Integrando estas técnicas, logramos que las librerías legacy se comporten como código moderno de Kotlin, aprovechando el control preciso sobre el ciclo de vida mediante scopes como lifecycleScope o viewModelScope, lo que garantiza que ninguna tarea quede ejecutándose en el vacío una vez que el usuario abandona la aplicación.

Continúar leyendo...