Esta pagina se ve mejor con JavaScript habilitado

Convirtiendo Callbacks a Corrutinas y Flows en Kotlin

 ·  ☕ 5 minutos lectura  ·  ✍️ Ignacio Carrión

Convirtiendo Callbacks a Coroutines y Flows en Kotlin

Las APIs basadas en callbacks han sido un patrón común en la programación asíncrona durante muchos años. Sin embargo, con las corrutinas y flows de Kotlin, podemos transformar estos callbacks en código moderno y secuencial que es más fácil de leer y mantener. En este artículo, exploraremos cómo usar suspendCoroutine y callbackFlow para convertir APIs basadas en callbacks a corrutinas y flows.

Entendiendo suspendCoroutine

La función suspendCoroutine es una herramienta poderosa que nos permite envolver APIs basadas en callbacks en funciones suspend. Esta transformación hace que el código asíncrono sea más secuencial y fácil de manejar.

Uso Básico de suspendCoroutine

Aquí hay un ejemplo simple de cómo convertir una función basada en callbacks a una función suspend:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// Traditional callback-based API
interface LocationCallback {
    fun onLocationFound(location: Location)
    fun onError(error: Exception)
}

class LocationService {
    fun getCurrentLocation(callback: LocationCallback) {
        // Simulating async location fetch
        // Implementation details...
    }
}

// Converted to suspend function
suspend fun LocationService.getLocationSuspend(): Location {
    return suspendCoroutine<Location> { continuation: Continuation<Location> ->
        getCurrentLocation(object : LocationCallback {
            override fun onLocationFound(location: Location) {
                continuation.resume(location)
            }

            override fun onError(error: Exception) {
                continuation.resumeWithException(error)
            }
        })
    }
}

// Usage
suspend fun fetchLocation() {
    try {
        val location = locationService.getLocationSuspend()
        println("Ubicación recibida: $location")
    } catch (e: Exception) {
        println("Error al obtener la ubicación: ${e.message}")
    }
}

Manejo de Cancelación

Cuando trabajamos con suspendCoroutine, es importante manejar la cancelación adecuadamente:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
suspend fun LocationService.getLocationSuspendWithCancellation(): Location {
    return suspendCancellableCoroutine<Location> { continuation: CancellableContinuation<Location> ->
        val callback = object : LocationCallback {
            override fun onLocationFound(location: Location) {
                continuation.resume(location)
            }

            override fun onError(error: Exception) {
                continuation.resumeWithException(error)
            }
        }

        getCurrentLocation(callback)

        continuation.invokeOnCancellation {
            // Cleanup resources, remove callbacks, etc.
            removeLocationUpdates(callback)
        }
    }
}

Convirtiendo a Flows con callbackFlow

Mientras que suspendCoroutine es excelente para operaciones únicas, callbackFlow es perfecto para manejar flujos de datos o eventos. Nos permite convertir APIs basadas en callbacks que emiten múltiples valores en Kotlin Flows.

Ejemplo Básico de callbackFlow

Aquí te mostramos cómo convertir una API de actualizaciones de ubicación a un Flow:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
interface LocationUpdatesCallback {
    fun onLocationUpdate(location: Location)
    fun onError(error: Exception)
}

class LocationService {
    fun startLocationUpdates(callback: LocationUpdatesCallback) {
        // Implementation details...
    }

    fun stopLocationUpdates(callback: LocationUpdatesCallback) {
        // Implementation details...
    }
}

fun LocationService.locationUpdatesFlow(): Flow<Location> = callbackFlow {
    val callback = object : LocationUpdatesCallback {
        override fun onLocationUpdate(location: Location) {
            trySend(location)
        }

        override fun onError(error: Exception) {
            close(error)
        }
    }

    startLocationUpdates(callback)

    // Clean up when the flow is cancelled
    awaitClose {
        stopLocationUpdates(callback)
    }
}

// Usage
suspend fun trackLocation() {
    locationService.locationUpdatesFlow()
        .catch { error: Throwable -> 
            println("Error en actualizaciones de ubicación: ${error.message}")
        }
        .collect { location: Location ->
            println("Nueva ubicación: $location")
        }
}

Manejo de Contrapresión (Backpressure)

Cuando tratamos con actualizaciones frecuentes, es importante manejar la contrapresión adecuadamente:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
fun SensorService.sensorUpdatesFlow(): Flow<SensorData> = callbackFlow {
    val callback = object : SensorCallback {
        override fun onSensorUpdate(data: SensorData) {
            // Use trySend instead of send to handle backpressure
            if (!trySend(data).isSuccess) {
                // Handle unsuccessful send
                println("Buffer full, dropping sensor update")
            }
        }
    }

    registerSensorCallback(callback)

    awaitClose {
        unregisterSensorCallback(callback)
    }
}.buffer(Channel.CONFLATED) // Keep only the latest value

Mejores Prácticas

  1. Manejo de Errores

    • Siempre manejar errores apropiadamente tanto en suspendCoroutine como en callbackFlow
    • Usar bloques try-catch para suspendCoroutine
    • Usar el operador catch para flows
  2. Gestión de Recursos

    • Limpiar recursos en awaitClose para callbackFlow
    • Usar suspendCancellableCoroutine cuando se necesite manejar cancelación
  3. Consideraciones de Contrapresión

    • Elegir estrategias de buffer apropiadas para tu caso de uso
    • Considerar usar canales conflated o buffered según tus necesidades
  4. Pruebas

    • Escribir pruebas tanto para escenarios de éxito como de error
    • Probar el comportamiento de cancelación
    • Verificar la limpieza de recursos

Patrones Comunes y Ejemplos

Manejo de Timeout

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
suspend fun apiCallWithTimeout(): Result<String> = 
    withTimeout(5000L) {
        suspendCoroutine<Result<String>> { continuation: Continuation<Result<String>> ->
            api.call(object : ApiCallback {
                override fun onSuccess(result: Result<String>) {
                    continuation.resume(result)
                }

                override fun onError(error: Exception) {
                    continuation.resumeWithException(error)
                }
            })
        }
    }

Combinando Múltiples Callbacks

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
fun MultipleSourceService.combinedUpdatesFlow(): Flow<Update> = callbackFlow {
    val callback1 = object : SourceCallback {
        override fun onUpdate(data: Update) {
            trySend(data)
        }
    }

    val callback2 = object : SourceCallback {
        override fun onUpdate(data: Update) {
            trySend(data)
        }
    }

    registerCallbacks(callback1, callback2)

    awaitClose {
        unregisterCallbacks(callback1, callback2)
    }
}.buffer(Channel.BUFFERED)

Conclusión

La conversión de APIs basadas en callbacks a corrutinas y flows puede mejorar significativamente la legibilidad y mantenibilidad del código. Usando suspendCoroutine para operaciones únicas y callbackFlow para flujos de datos, puedes modernizar código legacy y aprovechar al máximo las potentes características de concurrencia de Kotlin.

Recuerda siempre manejar los errores apropiadamente, gestionar los recursos correctamente y considerar la contrapresión cuando trates con actualizaciones de alta frecuencia. Con estas herramientas y patrones, puedes cerrar efectivamente la brecha entre las APIs basadas en callbacks y la concurrencia moderna de Kotlin.

compartir en

Ignacio Carrión
Escrito por
Ignacio Carrión
Android Developer