Channel es un primitivo de sincronización de la biblioteca Kotlin Coroutines para transferir datos entre corutinas. Según Kotlin Documentation, 2025, Channel implementa el patrón productor-consumidor con envío bloqueante a través de funciones suspend. Channel soporta los modos Rendezvous, Buffered y Conflated, cada uno de los cuales define el comportamiento al desbordarse.
Puntos Clave
Channel es conceptualmente similar a BlockingQueue de Java, pero con funciones suspend send() y receive() en lugar de put() y take() bloqueantes. Un desarrollador de Kotlin usa Channel para organizar el intercambio de datos entre corutinas sin sincronización a través de memoria compartida. El canal garantiza una entrega ordenada — el orden de envío coincide con el orden de recepción.
Para crear un Channel se llama a la función de fábrica Channel<T>(capacity). El parámetro capacity determina el tipo de canal: RENDEZVOUS (0), UNLIMITED (Int.MAX_VALUE), CONFLATED (-1) o un número específico. El tipo de elemento T se especifica mediante genéricos. Cerrar el canal con close() señala que no habrá más elementos nuevos.
send(value) es una función suspend que suspende la corutina emisora si el canal está lleno. receive() es una función suspend que suspende al receptor si el canal está vacío. Las alternativas trySend() y tryReceive() son versiones no bloqueantes que devuelven Boolean o null cuando la operación no es posible. Son útiles en contextos no suspend.
Kotlin proporciona cuatro variantes de Channel según la capacidad del búfer: Rendezvous (capacidad 0), Buffered (capacidad N), Conflated (capacidad 1, sobrescritura) y Unlimited (capacidad Int.MAX_VALUE). Cada tipo resuelve su propia tarea, desde la sincronización estricta hasta el almacenamiento masivo en búfer de datos.
Rendezvous Channel es el más estricto: send() se bloquea hasta que se llama a receive() en otra corutina. En esencia, es un punto de encuentro de dos corutinas. Ideal para un handshake estricto cuando el emisor debe esperar a que el receptor procese el elemento. Se descarta la pérdida de datos — send no se completa hasta que se ejecuta receive.
Conflated Channel almacena solo el último valor enviado. Si el emisor coloca un nuevo elemento antes de que el receptor recoja el antiguo, el antiguo se descarta. Conflated Channel es útil para el estado de la UI: si un usuario cambia rápidamente el control deslizante, los valores intermedios se pueden descartar y procesar solo el último.
El patrón clásico Productor-Consumidor en Channel se implementa mediante corutinas paralelas. Productor llama a send(value) en un bucle, consumidor llama a receive(value). El productor y el consumidor pueden trabajar en diferentes Dispatchers: productor en Dispatchers.IO, consumidor en Dispatchers.Main. Channel sincroniza automáticamente el acceso sin Lock ni synchronized.
Fan-out — múltiples consumidores en un solo canal. Cada elemento va exactamente a un consumidor (distribución round-robin). Fan-in — múltiples productores escriben en un solo canal. Las corutinas emisoras compiten por el envío, pero el orden de los elementos se conserva. Ambos escenarios no requieren sincronización adicional.
Produce es un constructor de corutinas que crea un canal con cierre automático. La función produce { } devuelve un ReceiveChannel — un canal de solo lectura para el consumidor. Dentro del constructor send() envía datos, y cuando el bloque se completa o se produce una excepción, el canal se cierra automáticamente, evitando fugas.
La biblioteca kotlinx.coroutines proporciona select — una expresión que espera el primer canal completado entre varias alternativas. Select permite multiplexar múltiples canales: por ejemplo, esperar datos de dos fuentes y procesar la que respondió primero. Sintaxis — select<T> { channel1.onReceive { } channel2.onReceive { } }. Es una alternativa al operador amb en Rx.
El primer ejemplo es un Rendezvous Channel simple donde el emisor espera la recepción:
val channel = Channel<String>()
scope.launch {
channel.send("Hello")
println("Enviado")
}
scope.launch {
val msg = channel.receive()
println("Recibido: $msg")
}
El segundo ejemplo — múltiples consumidores en un solo canal (fan-out):
val channel = Channel<Int>(Channel.UNLIMITED)
scope.launch {
for (x in 1..10) channel.send(x)
channel.close()
}
repeat(2) { id ->
scope.launch {
for (msg in channel) {
println("Consumer #$id: $msg")
}
}
}
El tercer ejemplo — uso del constructor produce con manejo de errores:
val source = produce {
for (i in 1..5) {
delay(200)
send(i)
}
}
scope.launch {
source
.consumeAsFlow()
.catch { println("Error: $it") }
.collect { println("Elemento: $it") }
}
Channel es un primitivo caliente: los datos se emiten independientemente de los suscriptores. Flow es frío: los datos se generan al suscribirse. Channel soporta múltiples productores y consumidores con entrega garantizada de cada elemento a un consumidor (fan-out). Flow no está diseñado para múltiples productores independientes.
Channel utiliza un búfer con capacidad configurable y funciones suspend send/receive para la gestión de contrapresión. Flow utiliza el mecanismo suspend collect con contrapresión automática a través de corutinas. Channel es una herramienta de bajo nivel para escenarios específicos: conversión de callbacks, modelo de actor, cola de tareas con múltiples emisores.
Para escenarios cotidianos en Android (estado de UI, streams reactivos de BD) Google recomienda Flow en lugar de Channel. Channel debe usarse cuando se necesita intercambio de datos caliente entre corutinas con control preciso del búfer, o al convertir interfaces callback mediante callbackFlow, cuya implementación interna usa Channel.
Un ejemplo práctico importante: al implementar un cliente WebSocket, Channel permite escribir mensajes desde una corutina y leer desde otra con la garantía de que cada mensaje se procesará exactamente una vez. Flow no es adecuado para esta tarea porque es frío y no soporta múltiples productores. Channel con capacidad UNLIMITED asegura que los mensajes entrantes no se pierdan durante demoras temporales del consumidor.
La gestión del ciclo de vida del canal es una parte importante del trabajo con Channel. El canal debe cerrarse cuando todos los datos se hayan enviado para que el consumidor pueda finalizar la iteración. Llamar a channel.close() señala que no habrá más elementos nuevos. El consumidor puede iterar mediante for (item in channel) — el bucle finalizará automáticamente después de close() y el agotamiento del búfer. Alternativamente, el consumidor puede llamar a receive() en un bucle con manejo de ClosedReceiveChannelException.
Channel se usa activamente en Android para implementar EventBus sin dependencias: un Channel<Event> global con una estrategia Broadcast permite enviar eventos desde cualquier punto de la aplicación. A diferencia de los buses basados en LiveData, Channel no está vinculado al ciclo de vida y no requiere limpieza al transicionar entre pantallas. send() desde ViewModel y receive() en Activity/Fragment mediante lifecycleScope proporcionan comunicación con seguridad de tipos sin clases Event. Múltiples consumidores en un Channel distribuyen la carga — cada elemento se procesa una vez, lo que evita la duplicación del manejo de un solo evento en diferentes suscriptores.
En sistemas de actores Channel sirve como base para implementar un mailbox — una cola de mensajes para el actor. Un actor es una corutina que lee mensajes de un Channel en un bucle y los procesa secuencialmente. Este enfoque garantiza que cada mensaje se procese en el orden de envío, sin condiciones de carrera. Kotlin no tiene un actor incorporado como tipo (a diferencia de Akka), pero Channel + launch es un reemplazo ligero.
Para el intercambio bidireccional se utilizan pares de canales: un canal para solicitudes del cliente al servidor, el segundo para respuestas del servidor al cliente. Por ejemplo, al implementar un Pipe en una aplicación multihilo: el productor escribe en OutputChannel, el consumidor lee de InputChannel. Las funciones suspend send y receive garantizan que el Productor-Consumidor no desbordará la pila de llamadas, ya que las corutinas se suspenden en lugar de bloquearse. Channel con capacidad BUFFERED es adecuado para la mayoría de escenarios donde la velocidad del productor y el consumidor son aproximadamente iguales. Para escenarios asimétricos use UNLIMITED, para que el productor no se suspenda cuando el consumidor está ocupado — esto reduce el riesgo de interbloqueo pero aumenta el consumo de memoria.
Al diseñar una arquitectura con canales es importante recordar capacity: la elección de la capacidad afecta directamente el comportamiento bajo carga máxima. Los canales con capacidad BUFFERED(N) actúan como un búfer de suavizado: si el consumidor es temporalmente más lento que el productor, los elementos se acumulan. Si la velocidad promedio del consumidor es consistentemente menor que la del productor, el búfer se llenará y la corutina emisora se suspenderá — esto es contrapresión automática que protege contra la sobrecarga de memoria.
Para monitoreo y depuración de Channel use kotlinx-coroutines-debug: la utilidad muestra la cantidad de corutinas activas, el estado de sus canales (abierto/cerrado, cantidad de elementos en el búfer) y la pila de llamadas de operaciones send/receive suspendidas. Channel también se puede envolver en un proxy de registro: la clase LoggingChannel<T> delega llamadas al Channel real, registrando las operaciones send, receive y close. Esto ayuda a identificar fugas de canal cuando no se llamó a close() y la corutina consumidora espera eternamente nuevos elementos.
Preguntas Frecuentes
Channel utiliza funciones suspend send() y receive() en lugar de put() y take() bloqueantes. A diferencia de BlockingQueue, Channel no bloquea el hilo al desbordarse — la corutina se suspende, liberando el hilo para otras corutinas. Esto es crítico para el uso eficiente de hilos en Kotlin.
Al llamar a send() en un canal cerrado se lanza ClosedSendChannelException. Antes de enviar verifique isClosedForSend o use trySend() que devuelve false cuando está cerrado. close() garantiza que los elementos ya enviados se recibirán antes de que se lance la excepción.
Conflated Channel es útil para eventos donde solo importa el último estado — barra de progreso, posición del control deslizante, coordenadas táctiles. Si el consumidor no puede procesar todos los eventos, los intermedios se descartan y el último se procesa garantizadamente. Conflated Channel tiene capacity=-1.
Llame a channel.close() — el canal se marca como cerrado para envío, pero los elementos ya enviados continúan leyéndose mediante receive(). La iteración con for (item in channel) finaliza automáticamente después de agotar el búfer. isClosedForSend devuelve true inmediatamente, isClosedForReceive devuelve true después del agotamiento.
No siempre. Flow es frío — una emisión por cada collect. Si se necesitan múltiples productores independientes escribiendo en un solo stream, Channel es obligatorio. Para transferencia simple de datos entre dos corutinas use Channel. Para streams reactivos con datos use Flow.
Resumen
Desarrollaremos una aplicación móvil llave en mano
IT Sectr crea aplicaciones para iOS y Android para startups y empresas desde 2017. Le asesoraremos y le propondremos la mejor solución.
Lea también