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

  1. 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.

  2. Esquecer de fechar Channels: um Channel não fechado pode causar coroutines suspensas para sempre. Sempre use close() ou considere usar produce {} que fecha automaticamente.

  3. 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.

  4. Ignorar o operador catch: sem tratamento de erros, exceções em Flows podem derrubar sua aplicação. Sempre adicione catch {} antes de collect {}.

  5. Confundir flowOn com launchIn: o flowOn muda o contexto de execução do upstream, enquanto launchIn define o escopo para o coletor. Usar ambos incorretamente pode levar a bugs sutis.

  6. Não usar stateIn ou shareIn em ViewModels: converter um cold Flow em StateFlow ou SharedFlow com stateIn ou shareIn evita 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: