Channel é um primitivo de sincronização da biblioteca Kotlin Coroutines para transferir dados entre corrotinas. De acordo com Kotlin Documentation, 2025, o Channel implementa o padrão produtor-consumidor com envio bloqueante através de funções suspend. Channel suporta os modos Rendezvous, Buffered e Conflated, cada um definindo o comportamento quando ocorre transbordamento.
Pontos Principais
Channel é conceitualmente semelhante ao BlockingQueue do Java, mas com funções suspend send() e receive() em vez de put() e take() bloqueantes. Um desenvolvedor Kotlin usa Channel para organizar a troca de dados entre corrotinas sem sincronização através de memória compartilhada. O canal garante entrega ordenada — a ordem de envio coincide com a ordem de recebimento.
Para criar um Channel, a função de fábrica Channel<T>(capacity) é chamada. O parâmetro capacity determina o tipo de canal: RENDEZVOUS (0), UNLIMITED (Int.MAX_VALUE), CONFLATED (-1) ou um número específico. O tipo do elemento T é especificado via genéricos. Fechar o canal com close() sinaliza que não haverá novos elementos.
send(value) é uma função suspend que suspende a corrotina remetente se o canal estiver cheio. receive() é uma função suspend que suspende o receptor se o canal estiver vazio. As alternativas trySend() e tryReceive() são versões não bloqueantes que retornam Boolean ou null quando a operação não é possível. São úteis em contextos não suspend.
Kotlin fornece quatro variantes de Channel através da capacidade do buffer: Rendezvous (capacidade 0), Buffered (capacidade N), Conflated (capacidade 1, sobrescrita) e Unlimited (capacidade Int.MAX_VALUE). Cada tipo resolve sua própria tarefa, desde sincronização estrita até bufferização em massa de dados.
Rendezvous Channel é o mais estrito: send() bloqueia até que receive() seja chamado em outra corrotina. Em essência, é um ponto de encontro de duas corrotinas. Ideal para handshake estrito quando o remetente deve aguardar o receptor processar o elemento. Perda de dados é descartada — send não completa até que receive seja executado.
Conflated Channel armazena apenas o último valor enviado. Se o remetente colocar um novo elemento antes do receptor pegar o antigo, o antigo é descartado. Conflated Channel é útil para estado de UI: se um usuário muda rapidamente o controle deslizante, valores intermediários podem ser descartados e apenas o último processado.
O padrão clássico Produtor-Consumidor em Channel é implementado através de corrotinas paralelas. Produtor chama send(value) em um loop, consumidor chama receive(value). O produtor e o consumidor podem trabalhar em Dispatchers diferentes: produtor em Dispatchers.IO, consumidor em Dispatchers.Main. O Channel sincroniza automaticamente o acesso sem Lock ou synchronized.
Fan-out — múltiplos consumidores em um único canal. Cada elemento vai exatamente para um consumidor (distribuição round-robin). Fan-in — múltiplos produtores escrevem em um único canal. As corrotinas remetentes competem pelo envio, mas a ordem dos elementos é preservada. Ambos os cenários não requerem sincronização adicional.
Produce é um construtor de corrotinas que cria um canal com fechamento automático. A função produce { } retorna um ReceiveChannel — um canal somente leitura para o consumidor. Dentro do construtor, send() envia dados, e quando o bloco completa ou uma exceção ocorre, o canal é fechado automaticamente, evitando vazamentos.
A biblioteca kotlinx.coroutines fornece select — uma expressão que aguarda o primeiro canal completo entre várias alternativas. O Select permite multiplexar múltiplos canais: por exemplo, aguardar dados de duas fontes e processar a que respondeu primeiro. Sintaxe — select<T> { channel1.onReceive { } channel2.onReceive { } }. É uma alternativa ao operador amb no Rx.
O primeiro exemplo é um Rendezvous Channel simples onde o remetente aguarda o recebimento:
val channel = Channel<String>()
scope.launch {
channel.send("Hello")
println("Enviado")
}
scope.launch {
val msg = channel.receive()
println("Recebido: $msg")
}
O segundo exemplo — múltiplos consumidores em um único 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")
}
}
}
O terceiro exemplo — uso do construtor produce com tratamento de erros:
val source = produce {
for (i in 1..5) {
delay(200)
send(i)
}
}
scope.launch {
source
.consumeAsFlow()
.catch { println("Erro: $it") }
.collect { println("Elemento: $it") }
}
Channel é um primitivo quente: os dados são emitidos independentemente dos assinantes. Flow é frio: os dados são gerados na assinatura. O Channel suporta múltiplos produtores e consumidores com entrega garantida de cada elemento a um consumidor (fan-out). O Flow não foi projetado para múltiplos produtores independentes.
O Channel usa um buffer com capacidade configurável e funções suspend send/receive para gerenciamento de contrapressão. O Flow usa o mecanismo suspend collect com contrapressão automática através de corrotinas. Channel é uma ferramenta de baixo nível para cenários específicos: conversão de callbacks, modelo de ator, fila de tarefas com múltiplos remetentes.
Para cenários cotidianos no Android (estado de UI, streams reativos do BD) o Google recomenda Flow em vez de Channel. O Channel deve ser usado quando é necessária troca de dados quente entre corrotinas com controle preciso de buffer, ou ao converter interfaces callback via callbackFlow, cuja implementação interna usa Channel.
Um exemplo prático importante: ao implementar um cliente WebSocket, o Channel permite escrever mensagens de uma corrotina e ler de outra com a garantia de que cada mensagem será processada exatamente uma vez. O Flow não é adequado para esta tarefa porque é frio e não suporta múltiplos produtores. O Channel com capacidade UNLIMITED garante que mensagens recebidas não sejam perdidas durante atrasos temporários do consumidor.
O gerenciamento do ciclo de vida do canal é uma parte importante do trabalho com Channel. O canal deve ser fechado quando todos os dados forem enviados para que o consumidor possa finalizar a iteração. Chamar channel.close() sinaliza que não haverá novos elementos. O consumidor pode iterar via for (item in channel) — o loop terminará automaticamente após close() e esvaziamento do buffer. Alternativamente, o consumidor pode chamar receive() em um loop com tratamento de ClosedReceiveChannelException.
O Channel é usado ativamente no Android para implementar EventBus sem dependências: um Channel<Event> global com estratégia Broadcast permite enviar eventos de qualquer ponto da aplicação. Ao contrário dos barramentos baseados em LiveData, o Channel não está vinculado ao ciclo de vida e não requer limpeza ao transicionar entre telas. send() da ViewModel e receive() na Activity/Fragment via lifecycleScope fornecem comunicação type-safe sem classes Event. Múltiplos consumidores em um Channel distribuem a carga — cada elemento é processado uma vez, evitando duplicação de tratamento de um único evento em diferentes assinantes.
Em sistemas de atores, Channel serve como base para implementar um mailbox — uma fila de mensagens para o ator. Um ator é uma corrotina que lê mensagens de um Channel em um loop e as processa sequencialmente. Esta abordagem garante que cada mensagem seja processada na ordem de envio, sem condições de corrida. Kotlin não tem um ator embutido como tipo (ao contrário de Akka), mas Channel + launch é uma substituição leve.
Para troca bidirecional, pares de canais são usados: um canal para requisições do cliente ao servidor, o segundo para respostas do servidor ao cliente. Por exemplo, ao implementar um Pipe em uma aplicação multithread: o produtor escreve no OutputChannel, o consumidor lê do InputChannel. As funções suspend send e receive garantem que o Produtor-Consumidor não transbordará a pilha de chamadas, pois as corrotinas suspendem em vez de bloquear. Channel com capacidade BUFFERED é adequado para a maioria dos cenários onde a velocidade do produtor e consumidor são aproximadamente iguais. Para cenários assimétricos, use UNLIMITED para que o produtor não suspenda quando o consumidor está ocupado — isso reduz o risco de deadlock mas aumenta o consumo de memória.
Ao projetar uma arquitetura com canais, é importante lembrar de capacity: a escolha da capacidade afeta diretamente o comportamento sob carga máxima. Canais com capacidade BUFFERED(N) atuam como um buffer de suavização: se o consumidor é temporariamente mais lento que o produtor, os elementos se acumulam. Se a velocidade média do consumidor é consistentemente menor que a do produtor, o buffer encherá e a corrotina remetente será suspensa — isso é contrapressão automática que protege contra sobrecarga de memória.
Para monitoramento e depuração do Channel, use kotlinx-coroutines-debug: a utilidade mostra o número de corrotinas ativas, o estado de seus canais (aberto/fechado, número de elementos no buffer) e a pilha de chamadas de operações send/receive suspensas. O Channel também pode ser encapsulado em um proxy de registro: a classe LoggingChannel<T> delega chamadas ao Channel real, registrando operações send, receive e close. Isso ajuda a identificar vazamentos de canal quando close() não foi chamado e a corrotina consumidora espera eternamente por novos elementos.
Perguntas Frequentes
Channel usa funções suspend send() e receive() em vez de put() e take() bloqueantes. Ao contrário do BlockingQueue, o Channel não bloqueia a thread ao transbordar — a corrotina suspende, liberando a thread para outras corrotinas. Isso é crítico para o uso eficiente de threads em Kotlin.
Ao chamar send() em um canal fechado, uma ClosedSendChannelException é lançada. Antes de enviar, verifique isClosedForSend ou use trySend() que retorna false quando fechado. close() garante que os elementos já enviados serão recebidos antes da exceção ser lançada.
Conflated Channel é útil para eventos onde apenas o estado mais recente importa — barra de progresso, posição do controle deslizante, coordenadas de toque. Se o consumidor não conseguir processar todos os eventos, os intermediários são descartados e o mais recente é garantidamente processado. Conflated Channel tem capacity=-1.
Chame channel.close() — o canal é marcado como fechado para envio, mas os elementos já enviados continuam sendo lidos via receive(). A iteração com for (item in channel) termina automaticamente após o esvaziamento do buffer. isClosedForSend retorna true imediatamente, isClosedForReceive retorna true após o esvaziamento.
Nem sempre. Flow é frio — uma emissão por cada collect. Se múltiplos produtores independentes escrevendo em um único stream são necessários, o Channel é obrigatório. Para transferência simples de dados entre duas corrotinas, use Channel. Para streams reativos com dados, use Flow.
Resumo
Vamos desenvolver um aplicativo móvel chave na mão
A IT Sectr cria aplicativos para iOS e Android para startups e empresas desde 2017. Nós vamos aconselhá-lo e propor a melhor solução.
Leia também