Si llevas un tiempo picando código en Android, seguro que te has topado con el bendito «callback hell». Esas cadenas de funciones anidadas que parecen una escalera y que hacen que leer el código sea un auténtico quebradero de cabeza, especialmente cuando intentas rastrear dónde se ha colado un error. La mayoría de los SDKs antiguos y las APIs del sistema siguen usando este patrón, obligándonos a lidiar con una estructura que no encaja con la programación secuencial moderna.
Por suerte, Kotlin nos ofrece un puente directo para dejar atrás estas reliquias y pasar ourselves a un modelo de concurrencia estructurada. Transformando estos callbacks en funciones suspendidas o flujos de datos, conseguimos que el código sea mucho más legible, fácil de testear y, sobre todo, mantenible a largo plazo, evitando que nuestra aplicación se convierta en un laberinto de respuestas asíncronas.
El puente fundamental: suspendCancellableCoroutine
Cuando necesitamos que una función espere a que un callback responda una sola vez para devolver un valor, tenemos dos opciones: suspendCoroutine y suspendCancellableCoroutine. Aquí es donde hay que tener mucho ojo, porque aunque se parecen, no deben usarse indistintamente. La versión básica no está ligada a la cancelación de la corrutina, lo que significa que si el Job padre se cancela, la corrutina se queda ahí colgada, reteniendo memoria y recursos hasta que el callback eventualmente responda (si es que lo hace).
Para evitar estos agujeros en la concurrencia, debemos optar siempre por suspendCancellableCoroutine. Esta herramienta nos entrega un CancellableContinuation que sí participa en la jerarquía de cancelación. Si el usuario navega fuera de la pantalla o el ViewModel se destruye, la corrutina se libera inmediatamente. Para que esto sea perfecto, es vital usar el bloque invokeOnCancellation, donde podemos cerrar conexiones o liberar recursos del SDK para que no queden vivos en segundo plano.
Implementando el patrón de puente
El proceso es básicamente un baile de tres pasos. Primero, pausamos la ejecución con la función suspendible; segundo, ejecutamos la API de callback pasando una implementación anónima que capture nuestra continuación; y tercero, llamamos a resume o resumeWithException para despertar la corrutina con el resultado. Un detalle crítico es que solo debemos llamar a resume una vez; si lo hacemos dos veces, la app lanzará una excepción de estado ilegal, y si no lo hacemos nunca, la corrutina se quedará dormida para siempre.
Gestión avanzada de errores y resultados
No todos los callbacks son tan simples como devolver un booleano. Muchos SDKs separan el éxito del error en métodos distintos. En lugar de lanzar una excepción genérica que borre la información útil, lo ideal es diseñar una jerarquía de excepciones tipadas. Por ejemplo, crear una clase base de excepción que envuelva el código de error original del SDK, permitiendo que quien llame a la función pueda usar un bloque when para decidir si debe mostrar un diálogo de reintento o un mensaje de error crítico.
A veces, el uso de try-catch puede resultar engorroso. En esos casos, es muy elegante devolver un Result<T>. En lugar de usar resumeWithException, llamamos a resume(Result.failure(...)). De este modo, la función técnicamente completa su ejecución con éxito, devolviendo un objeto que el programador puede procesar usando fold o onFailure, dando mucha más flexibilidad según el estilo de arquitectura que se esté siguiendo.
Cuando los datos no paran: callbackFlow
Si el callback no es de un solo disparo, sino que es un listener que emite valores constantemente (como los cambios de ubicación del GPS o actualizaciones de Firebase), suspendCancellableCoroutine no nos sirve. Para estos escenarios, el callbackFlow es la herramienta definitiva. Este crea un canal que puede enviar múltiples valores a través de la función trySend, transformando un flujo de eventos en un Stream de Kotlin.
El punto más crítico aquí es el uso de awaitClose. Sin este bloque, el Flow se cerraría inmediatamente después de registrar el listener, o peor aún, el listener se quedaría activo eternamente provocando una fuga de memoria masiva. En awaitClose es donde debemos desregistrar el listener o cerrar el socket. Para manejar la contrapresión (backpressure) cuando los datos llegan más rápido de lo que podemos procesarlos, podemos aplicar operadores como buffer con la estrategia DROP_OLDEST o usar sample para tomar solo una muestra cada cierto tiempo.
Ejemplos prácticos de integración
- Firebase: Podemos envolver un
ValueEventListeneren uncallbackFlow, emitiendo cadaDataSnapshoty cerrando el flujo en el evento de cancelación. - LocationManager: Convertimos el
LocationListeneren un Flow, asegurando que elremoveUpdatesse ejecute en elawaitClosepara no destrozar la batería del dispositivo. - Retrofit/OkHttp: Si no podemos usar funciones suspendidas nativas, podemos envolver un
Callen un Flow que emita la respuesta y se cierre inmediatamente después del primer resultado.
Estrategias de robustez y testing
Para que nuestro puente sea realmente profesional, podemos añadir capas de conveniencia. En lugar de escribir el objeto callback en cada función, podemos crear factorías de callbacks que conviertan pares de lambdas (éxito y error) en la interfaz que el SDK requiera. Esto limpia muchísimo la vista del código. Además, es recomendable implementar reintentos con backoff exponencial usando el operador retryWhen, evitando saturar el servidor si hay fallos temporales de red.
A la hora de probar este código, herramientas como Turbine son fundamentales. Nos permiten testear Flows de forma secuencial, verificando que los elementos emitidos por el callback lleguen en el orden correcto y que los errores se propaguen adecuadamente sin bloquear el hilo principal.
La clave para modernizar cualquier API basada en callbacks reside en elegir la herramienta adecuada: la función suspendible para respuestas únicas y el flujo para eventos continuos. Al integrar la cancelación estructurada y una gestión de errores tipada, convertimos un código legacy propenso a errores en un sistema robusto, predecible y extremadamente limpio que aprovecha todo el potencial de Kotlin. Comparte esta información para que más personas conozcan del tema.