Neste tutorial completo, você vai aprender tudo sobre Kotlin Flow, a API de programação reativa do Kotlin para lidar com fluxos de dados assíncronos. Vamos cobrir desde os conceitos fundamentais de cold streams até tópicos avançados como StateFlow, SharedFlow, operadores de transformação e tratamento de exceções. Ao final, você terá domínio suficiente para aplicar Flow em projetos Android e backend com confiança.
O que é Kotlin Flow?
O Flow é uma API da biblioteca kotlinx.coroutines que permite trabalhar com sequências de valores emitidos de forma assíncrona. Ele é classificado como um cold stream — isso significa que o código produtor só é executado quando existe um coletor (collector) consumindo os dados. Essa característica diferencia o Flow de outras abordagens como Channels, que são hot streams.
Para quem vem do mundo RxJava, o Flow é a alternativa nativa do Kotlin para programação reativa, com a vantagem de ser muito mais leve, integrado com Coroutines e com tipagem segura graças ao sistema de tipos do Kotlin.
Passo 1: Criando seu Primeiro Flow com o Builder
O jeito mais comum de criar um Flow é usando o builder flow {}. Dentro dele, você usa a função suspend emit() para enviar valores ao coletor.
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.delay
fun contagem(): Flow<Int> = flow {
for (i in 1..5) {
delay(500) // simula trabalho assíncrono
emit(i) // emite o valor
}
}
fun main() = runBlocking {
contagem().collect { valor ->
println("Valor recebido: $valor")
}
}
Note que collect é uma função terminal — ela é suspend e bloqueia a coroutine atual até que o Flow termine de emitir todos os valores. O Flow respeita a structured concurrency, sendo cancelado automaticamente quando o escopo da coroutine é encerrado.
Existem também builders simplificados para casos comuns:
// Flow a partir de uma lista
val flowDeLista = listOf(1, 2, 3).asFlow()
// Flow com um único valor
val flowUnico = flowOf("Olá, Kotlin Flow!")
// Flow vazio
val flowVazio = emptyFlow<String>()
Passo 2: Operadores Intermediários — map, filter e transform
Assim como trabalhamos com Collections, o Flow oferece operadores intermediários que transformam os dados antes de chegarem ao coletor. Esses operadores retornam um novo Flow e são avaliados de forma preguiçosa (lazy).
fun main() = runBlocking {
(1..10).asFlow()
.filter { it % 2 == 0 } // mantém apenas pares
.map { it * it } // eleva ao quadrado
.collect { println(it) } // 4, 16, 36, 64, 100
}
O operador transform é mais flexível, permitindo emitir zero ou mais valores para cada item recebido:
fun main() = runBlocking {
(1..3).asFlow()
.transform { valor ->
emit("Processando $valor...")
delay(300)
emit("Resultado: ${valor * 10}")
}
.collect { println(it) }
}
Outros operadores úteis incluem take (limita quantidade de emissões), drop (ignora as primeiras N emissões), distinctUntilChanged (evita valores duplicados consecutivos) e onEach (executa uma ação para cada valor sem transformá-lo).
Passo 3: Controlando o Contexto com flowOn
Por padrão, o Flow executa no contexto da coroutine que chama collect. Mas e se você quiser que o código produtor rode em uma thread diferente? É aí que entra o operador flowOn.
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.runBlocking
fun dadosPesados(): Flow<Int> = flow {
for (i in 1..5) {
Thread.sleep(500) // simula operação de I/O
emit(i)
println("Emitindo em: ${Thread.currentThread().name}")
}
}.flowOn(Dispatchers.IO) // muda o contexto do produtor
fun main() = runBlocking {
dadosPesados().collect { valor ->
println("Coletando $valor em: ${Thread.currentThread().name}")
}
}
O flowOn altera apenas o contexto upstream (do produtor), não do coletor. Isso mantém a previsibilidade do código e respeita o princípio de transparência de contexto do Flow.
Passo 4: Buffer e Conflate para Performance
Quando o produtor é mais rápido que o coletor, podemos usar buffer para permitir que eles executem concorrentemente, ou conflate para descartar valores intermediários e processar apenas o mais recente.
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.delay
fun main() = runBlocking {
val tempoInicio = System.currentTimeMillis()
(1..5).asFlow()
.onEach { delay(100) } // produtor emite a cada 100ms
.buffer() // permite execução concorrente
.collect { valor ->
delay(300) // coletor leva 300ms para processar
val tempo = System.currentTimeMillis() - tempoInicio
println("$valor coletado em ${tempo}ms")
}
}
Com conflate, se o coletor estiver ocupado, valores intermediários são descartados:
(1..5).asFlow()
.onEach { delay(100) }
.conflate() // descarta valores não processados
.collect { valor ->
delay(300)
println("Processado: $valor") // pode pular 2, 3, 4
}
Passo 5: StateFlow e SharedFlow — Hot Streams
Enquanto o Flow padrão é cold, o Kotlin oferece duas variantes hot para cenários onde múltiplos coletores precisam observar o mesmo fluxo de dados.
StateFlow mantém sempre o valor mais recente e emite para novos coletores imediatamente. É ideal para representar estado em arquiteturas como MVVM.
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.delay
fun main() = runBlocking {
val estado = MutableStateFlow(0) // valor inicial obrigatório
// Coletor 1
val job1 = launch {
estado.collect { println("Coletor 1: $it") }
}
delay(100)
estado.value = 1
delay(100)
estado.value = 2
// Coletor 2 recebe imediatamente o valor atual (2)
val job2 = launch {
estado.collect { println("Coletor 2: $it") }
}
delay(100)
estado.value = 3
delay(200)
job1.cancel()
job2.cancel()
}
SharedFlow é mais flexível — permite configurar replay (quantos valores anteriores novos coletores recebem) e não exige valor inicial:
val eventos = MutableSharedFlow<String>(
replay = 1, // novos coletores recebem o último evento
extraBufferCapacity = 5
)
// Emitir valor
eventos.emit("Evento A")
// Ou de forma não-suspensa (pode falhar se o buffer estiver cheio)
eventos.tryEmit("Evento B")
Passo 6: Tratamento de Exceções com catch
O operador catch intercepta exceções que ocorrem upstream no Flow. Ele não captura erros no collect, apenas nos operadores e no produtor acima dele na cadeia.
fun fluxoComErro(): Flow<Int> = flow {
emit(1)
emit(2)
throw RuntimeException("Algo deu errado!")
emit(3) // nunca será alcançado
}
fun main() = runBlocking {
fluxoComErro()
.catch { e ->
println("Erro capturado: ${e.message}")
emit(-1) // pode emitir um valor de fallback
}
.collect { println("Valor: $it") }
// Saída: Valor: 1, Valor: 2, Erro capturado: Algo deu errado!, Valor: -1
}
Para capturar erros no collect, use onEach junto com catch e launchIn:
fluxoComErro()
.onEach { valor -> println("Processando: $valor") }
.catch { e -> println("Erro: ${e.message}") }
.launchIn(this) // equivale a launch { flow.collect() }
Você também pode usar onCompletion para executar código quando o Flow termina, independentemente de ter ocorrido erro:
(1..3).asFlow()
.onCompletion { causa ->
if (causa != null) println("Terminou com erro: $causa")
else println("Flow completado com sucesso")
}
.collect { println(it) }
Erros Comuns
Emitir valores de outro contexto sem flowOn: Nunca use
withContextdentro do builderflow {}para mudar o dispatcher. O Flow proíbe isso e lança umaIllegalStateException. UseflowOnem vez disso.Esquecer que collect é suspend: O
collectsuspende a coroutine. Se você precisa coletar sem bloquear, uselaunchIn(scope)em vez decollect.Confundir StateFlow com LiveData: O StateFlow sempre emite o valor atual para novos coletores, mas diferente do LiveData, ele não é lifecycle-aware. No Android, use
repeatOnLifecycleouflowWithLifecyclepara coletar de forma segura.Não tratar backpressure: Se o produtor é muito mais rápido que o consumidor e você não usa
bufferouconflate, o coletor será o gargalo e o Flow ficará lento.Usar SharedFlow sem buffer adequado: Se o
extraBufferCapacityfor 0 e nenhum coletor estiver ativo, chamadas aemit()vão suspender indefinidamente. Configure o buffer conforme sua necessidade.Ignorar o cancelamento: Flows respeitam o cancelamento cooperativo de coroutines. Se você usar operações bloqueantes (como
Thread.sleep) em vez dedelay, o cancelamento não funcionará corretamente.
Conclusão e Próximos Passos
Neste tutorial, você aprendeu os fundamentos e técnicas avançadas do Kotlin Flow: desde a criação de cold streams com o builder flow {}, passando por operadores de transformação como map, filter e transform, controle de contexto com flowOn, otimização de performance com buffer e conflate, hot streams com StateFlow e SharedFlow, até o tratamento robusto de exceções com catch.
O Flow é uma peça central no ecossistema Kotlin moderno e se integra perfeitamente com frameworks como Jetpack Compose, Room e Retrofit. Como próximos passos, recomendamos:
- Explorar a integração de Flow com Room Database para observar mudanças no banco de dados em tempo real
- Aprender sobre o padrão MVVM usando StateFlow no ViewModel
- Praticar combinando múltiplos Flows com operadores como
combine,zipemerge - Consultar o glossário de Flow e Coroutines para reforçar conceitos
Dominar o Kotlin Flow é um diferencial importante para qualquer desenvolvedor que trabalhe com Kotlin, seja no Android ou no backend.