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

  1. Emitir valores de outro contexto sem flowOn: Nunca use withContext dentro do builder flow {} para mudar o dispatcher. O Flow proíbe isso e lança uma IllegalStateException. Use flowOn em vez disso.

  2. Esquecer que collect é suspend: O collect suspende a coroutine. Se você precisa coletar sem bloquear, use launchIn(scope) em vez de collect.

  3. 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 repeatOnLifecycle ou flowWithLifecycle para coletar de forma segura.

  4. Não tratar backpressure: Se o produtor é muito mais rápido que o consumidor e você não usa buffer ou conflate, o coletor será o gargalo e o Flow ficará lento.

  5. Usar SharedFlow sem buffer adequado: Se o extraBufferCapacity for 0 e nenhum coletor estiver ativo, chamadas a emit() vão suspender indefinidamente. Configure o buffer conforme sua necessidade.

  6. Ignorar o cancelamento: Flows respeitam o cancelamento cooperativo de coroutines. Se você usar operações bloqueantes (como Thread.sleep) em vez de delay, 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, zip e merge
  • 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.