Neste tutorial, vamos explorar os conceitos avançados de Coroutines em Kotlin, com foco em Flow e Channel. Essas duas ferramentas são essenciais para lidar com fluxos de dados assíncronos de forma eficiente e elegante. Se você já domina o básico de coroutines, este é o próximo passo para elevar seu nível como desenvolvedor Kotlin.
O que são Flows em Kotlin?
O Flow é um tipo de stream assíncrono e frio (cold stream) que emite valores sequencialmente. Diferente de um Channel, o Flow só começa a produzir valores quando alguém os coleta. Isso o torna ideal para cenários em que você precisa processar uma sequência de dados de forma reativa.
Um Flow é declarado usando o builder flow {}, e cada valor é emitido com a função suspend emit(). O coletor recebe os valores usando collect {}.
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.runBlocking
fun numerosSimples(): Flow<Int> = flow {
for (i in 1..5) {
kotlinx.coroutines.delay(300)
emit(i)
}
}
fun main() = runBlocking {
numerosSimples().collect { valor ->
println("Recebido: $valor")
}
}
Neste exemplo, o Flow emite os números de 1 a 5, com um atraso de 300 milissegundos entre cada emissão. O collect é uma função terminal que consome os valores emitidos. Note que o Flow respeita a structured concurrency do Kotlin, ou seja, ele é cancelado automaticamente quando o escopo da coroutine é cancelado.
Os Flows são cold streams, o que significa que o código dentro do builder flow {} só é executado quando alguém chama collect. Isso é diferente de Channels, que são hot streams e podem produzir valores independentemente de haver um consumidor.
Operadores de Flow
Uma das maiores vantagens do Flow é a rica coleção de operadores intermediários disponíveis. Eles permitem transformar, filtrar e combinar fluxos de dados de maneira declarativa, semelhante ao que fazemos com Collections.
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.runBlocking
fun main() = runBlocking {
val fluxo = (1..20).asFlow()
fluxo
.filter { it % 2 == 0 }
.map { it * it }
.take(5)
.collect { valor ->
println("Valor transformado: $valor")
}
}
Alguns dos operadores mais utilizados são:
map: transforma cada valor emitido pelo Flow.filter: filtra valores com base em uma condição.take: limita a quantidade de valores emitidos.zip: combina dois Flows em pares.flatMapConcat: transforma cada valor em um novo Flow e os concatena.onEach: executa uma ação para cada valor sem alterá-lo.catch: intercepta exceções lançadas upstream.flowOn: altera o dispatcher em que o Flow é executado.
O operador flowOn é particularmente importante, pois permite que você mude o contexto de execução do Flow sem afetar o coletor. Por exemplo, você pode processar dados em Dispatchers.IO e coletar na thread principal.
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
fun buscarDados(): Flow<String> = flow {
emit("Dado 1")
emit("Dado 2")
emit("Dado 3")
}.flowOn(Dispatchers.IO)
fun main() = runBlocking {
buscarDados()
.catch { e -> println("Erro capturado: ${e.message}") }
.collect { dado ->
println("Coletado na Main: $dado")
}
}
Channels: Comunicação entre Coroutines
Enquanto o Flow é um cold stream, o Channel é um hot stream que permite a comunicação entre coroutines. Pense em um Channel como uma fila (queue) thread-safe que conecta produtores e consumidores de dados.
Channels são úteis quando você precisa de comunicação bidirecional entre coroutines ou quando deseja que o produtor funcione independentemente do consumidor. Existem diferentes tipos de Channel:
Channel.RENDEZVOUS(padrão): sem buffer, o produtor espera o consumidor estar pronto.Channel.BUFFERED: com buffer limitado (64 por padrão).Channel.UNLIMITED: buffer ilimitado, nunca suspende o produtor.Channel.CONFLATED: mantém apenas o último valor enviado.
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
fun main() = runBlocking {
val channel = Channel<Int>(capacity = Channel.BUFFERED)
launch {
for (i in 1..10) {
println("Enviando: $i")
channel.send(i)
delay(100)
}
channel.close()
}
launch {
for (valor in channel) {
println("Recebendo: $valor")
delay(250)
}
}
}
Neste exemplo, o produtor envia valores mais rápido do que o consumidor processa. Graças ao buffer, o produtor pode adiantar o envio de alguns valores sem precisar aguardar o consumidor. Quando o buffer está cheio, o produtor é suspenso automaticamente até que haja espaço.
É fundamental lembrar de fechar o Channel com close() quando não há mais dados a enviar. Caso contrário, o consumidor ficará suspenso indefinidamente esperando novos valores.
StateFlow e SharedFlow
O Kotlin também oferece dois tipos especiais de Flow para cenários de estado e eventos:
StateFlow é um Flow que mantém um único valor atual e emite atualizações para todos os coletores. Ele é ideal para representar estado em aplicações Android com Jetpack Compose ou ViewModels.
SharedFlow é mais flexível e permite configurar replay (quantidade de valores emitidos novamente para novos coletores) e buffer. Ele é perfeito para eventos que não devem ser perdidos.
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
class ContadorViewModel {
private val _contador = MutableStateFlow(0)
val contador: StateFlow<Int> = _contador.asStateFlow()
fun incrementar() {
_contador.value++
}
fun decrementar() {
_contador.value--
}
}
fun main() = runBlocking {
val viewModel = ContadorViewModel()
val job = launch {
viewModel.contador.collect { valor ->
println("Contador: $valor")
}
}
viewModel.incrementar()
delay(100)
viewModel.incrementar()
delay(100)
viewModel.decrementar()
delay(100)
job.cancel()
}
A diferença principal entre StateFlow e SharedFlow é que StateFlow sempre possui um valor inicial e utiliza distinctUntilChanged por padrão, ou seja, só emite quando o valor realmente muda. SharedFlow não tem valor inicial e pode emitir valores repetidos.
Dicas e Erros Comuns
Não coletar Flow na Main Thread sem cuidado: em aplicações Android, sempre colete Flows no escopo correto (
viewModelScope,lifecycleScope) para evitar memory leaks.Esquecer de fechar Channels: um Channel não fechado pode causar coroutines suspensas para sempre. Sempre use
close()ou considere usarproduce {}que fecha automaticamente.Usar Channel quando Flow basta: se você não precisa de comunicação bidirecional ou hot stream, prefira Flow. Ele é mais simples, seguro e compõe melhor com operadores.
Ignorar o operador
catch: sem tratamento de erros, exceções em Flows podem derrubar sua aplicação. Sempre adicionecatch {}antes decollect {}.Confundir
flowOncomlaunchIn: oflowOnmuda o contexto de execução do upstream, enquantolaunchIndefine o escopo para o coletor. Usar ambos incorretamente pode levar a bugs sutis.Não usar
stateInoushareInem ViewModels: converter um cold Flow em StateFlow ou SharedFlow comstateInoushareInevita múltiplas execuções desnecessárias quando há vários coletores.
Conclusão e Próximos Passos
Neste tutorial, exploramos os conceitos avançados de Coroutines em Kotlin, incluindo Flow, Channel, StateFlow e SharedFlow. Essas ferramentas formam a base da programação assíncrona moderna em Kotlin e são fundamentais para qualquer desenvolvedor que trabalhe com aplicações reativas.
Dominar Flows e Channels permite criar aplicações mais responsivas, com melhor gerenciamento de recursos e código mais limpo. A chave é entender quando usar cada ferramenta: Flow para streams frios e reativos, Channel para comunicação entre coroutines, StateFlow para estado observável e SharedFlow para eventos.
Para continuar sua jornada de aprendizado, recomendamos os seguintes tutoriais:
- Primeiro App Android com Kotlin para aplicar coroutines em um projeto real
- Jetpack Compose: Introdução para usar StateFlow com UI declarativa
- Testes Unitários com Kotlin para aprender a testar código assíncrono
- Criando API REST com Ktor para usar Flow em aplicações server-side