Kotlin Coroutines transformaram a forma como lidamos com programação assíncrona e concorrente na JVM. Antes das coroutines, desenvolvedores precisavam lidar com callbacks aninhados, RxJava complexo ou threads manuais para executar operações assíncronas. As coroutines oferecem uma abordagem sequencial para código assíncrono, onde funções suspensas podem ser pausadas e retomadas sem bloquear threads. Este guia cobre desde os fundamentos até padrões avançados que você vai usar em projetos reais, tanto no Android quanto no backend.

O Que São Coroutines

Uma coroutine é uma instancia de computacao suspensavel. Diferente de threads, que são gerenciadas pelo sistema operacional e consomem recursos significativos, coroutines são leves e gerenciadas pelo runtime do Kotlin. Você pode lancar milhares de coroutines sem impacto significativo no consumo de memória, algo impraticavel com threads tradicionais.

import kotlinx.coroutines.*

fun main() = runBlocking {
    launch {
        delay(1000L)
        println("Mundo!")
    }
    println("Ola,")
}
// Saida:
// Ola,
// Mundo!

O runBlocking cria um escopo de coroutine que bloqueia a thread atual até que todas as coroutines filhas terminem. O launch inicia uma nova coroutine sem bloquear, e delay e uma função suspensa que pausa a coroutine sem bloquear a thread.

Suspend Functions

Funções marcadas com suspend podem ser pausadas e retomadas. Elas só podem ser chamadas de dentro de outras funções suspensas ou de um escopo de coroutine:

suspend fun buscarUsuario(id: Long): Usuario {
    return withContext(Dispatchers.IO) {
        // Simula chamada de rede
        val resposta = apiService.buscarUsuario(id)
        resposta.toDomain()
    }
}

suspend fun buscarPedidos(usuarioId: Long): List<Pedido> {
    return withContext(Dispatchers.IO) {
        apiService.buscarPedidos(usuarioId).map { it.toDomain() }
    }
}

// Chamadas sequenciais
suspend fun carregarDashboard(usuarioId: Long): Dashboard {
    val usuario = buscarUsuario(usuarioId)
    val pedidos = buscarPedidos(usuarioId)
    return Dashboard(usuario, pedidos)
}

// Chamadas paralelas com async
suspend fun carregarDashboardParalelo(usuarioId: Long): Dashboard {
    return coroutineScope {
        val usuarioDeferred = async { buscarUsuario(usuarioId) }
        val pedidosDeferred = async { buscarPedidos(usuarioId) }
        Dashboard(usuarioDeferred.await(), pedidosDeferred.await())
    }
}

A versão paralela com async e await executa ambas as chamadas simultaneamente, reduzindo o tempo total de espera.

Dispatchers

Os Dispatchers determinam em qual thread ou pool de threads a coroutine sera executada:

// Dispatchers.Main - Thread principal (UI no Android)
// Dispatchers.IO - Pool otimizado para I/O (rede, disco)
// Dispatchers.Default - Pool otimizado para CPU (calculos)
// Dispatchers.Unconfined - Sem thread especifica

suspend fun exemploDispatchers() {
    // Operação de rede
    val dados = withContext(Dispatchers.IO) {
        apiService.buscarDados()
    }

    // Processamento pesado
    val resultado = withContext(Dispatchers.Default) {
        dados.map { item ->
            processarItem(item) // CPU intensivo
        }
    }

    // Atualizar UI (Android)
    withContext(Dispatchers.Main) {
        exibirResultado(resultado)
    }
}

O withContext troca o dispatcher dentro de uma função suspensa, permitindo executar cada operação no contexto mais adequado.

Escopos de Coroutine

Cada coroutine pertence a um escopo que gerencia seu ciclo de vida. No Android, os escopos mais comuns são viewModelScope e lifecycleScope:

class MinhaViewModel : ViewModel() {

    fun carregarDados() {
        // Cancelado automaticamente quando o ViewModel e destruido
        viewModelScope.launch {
            try {
                val resultado = repository.buscarDados()
                _estado.value = Estado.Sucesso(resultado)
            } catch (e: CancellationException) {
                throw e // Nunca engula CancellationException
            } catch (e: Exception) {
                _estado.value = Estado.Erro(e.message)
            }
        }
    }
}

// Escopo personalizado
class MeuServico {
    private val scope = CoroutineScope(
        SupervisorJob() + Dispatchers.Default
    )

    fun iniciar() {
        scope.launch {
            // Trabalho em background
        }
    }

    fun encerrar() {
        scope.cancel() // Cancela todas as coroutines
    }
}

Structured Concurrency

Structured concurrency garante que coroutines filhas sejam canceladas quando o escopo pai e cancelado, evitando leaks de coroutines:

suspend fun processarLote(itens: List<Item>) = coroutineScope {
    val resultados = itens.map { item ->
        async {
            processarItem(item)
        }
    }

    // Se qualquer async falhar, todas são canceladas
    resultados.awaitAll()
}

// Com SupervisorScope, falhas nao propagam para irmaos
suspend fun processarLoteIndependente(itens: List<Item>) = supervisorScope {
    val resultados = itens.map { item ->
        async {
            try {
                processarItem(item)
            } catch (e: Exception) {
                null // Falha individual nao cancela as demais
            }
        }
    }
    resultados.awaitAll().filterNotNull()
}

Kotlin Flow

Flow e a API para streams reativos das coroutines. Diferente do suspend, que retorna um único valor, Flow emite múltiplos valores ao longo do tempo:

// Criando um Flow
fun contadorFlow(): Flow<Int> = flow {
    var contador = 0
    while (true) {
        emit(contador++)
        delay(1000L)
    }
}

// Operadores de transformacao
fun buscarProdutosFlow(query: String): Flow<List<Produto>> {
    return flowOf(query)
        .debounce(300L)
        .filter { it.length >= 3 }
        .flatMapLatest { consulta ->
            repository.buscarProdutos(consulta)
        }
        .catch { e ->
            emit(emptyList())
        }
        .flowOn(Dispatchers.IO)
}

// Coletando um Flow
viewModelScope.launch {
    buscarProdutosFlow("kotlin")
        .collect { produtos ->
            _estado.value = Estado.Sucesso(produtos)
        }
}

StateFlow e SharedFlow

StateFlow e SharedFlow são versões especializadas de Flow para compartilhar estado e eventos:

class ConfigViewModel : ViewModel() {
    // StateFlow - sempre tem um valor atual
    private val _tema = MutableStateFlow(Tema.CLARO)
    val tema: StateFlow<Tema> = _tema.asStateFlow()

    // SharedFlow - para eventos que nao precisam de valor inicial
    private val _notificacoes = MutableSharedFlow<String>()
    val notificacoes: SharedFlow<String> = _notificacoes.asSharedFlow()

    fun alternarTema() {
        _tema.value = if (_tema.value == Tema.CLARO) Tema.ESCURO else Tema.CLARO
    }

    fun enviarNotificacao(mensagem: String) {
        viewModelScope.launch {
            _notificacoes.emit(mensagem)
        }
    }
}

Tratamento de Erros

O tratamento de erros em coroutines exige atencao especial:

// CoroutineExceptionHandler para erros nao tratados
val handler = CoroutineExceptionHandler { _, exception ->
    println("Erro capturado: ${exception.message}")
}

val scope = CoroutineScope(SupervisorJob() + handler)

// Try-catch em funcoes suspensas
suspend fun operacaoSegura(): Result<Dados> {
    return try {
        val dados = apiService.buscarDados()
        Result.success(dados)
    } catch (e: CancellationException) {
        throw e // NUNCA capture CancellationException
    } catch (e: HttpException) {
        Result.failure(e)
    } catch (e: IOException) {
        Result.failure(e)
    }
}

// Retry com backoff exponencial
suspend fun <T> retryComBackoff(
    tentativas: Int = 3,
    delayInicial: Long = 1000L,
    fator: Double = 2.0,
    bloco: suspend () -> T
): T {
    var delayAtual = delayInicial
    repeat(tentativas - 1) {
        try {
            return bloco()
        } catch (e: Exception) {
            delay(delayAtual)
            delayAtual = (delayAtual * fator).toLong()
        }
    }
    return bloco() // Ultima tentativa sem catch
}

Boas Práticas com Coroutines

  • Nunca capture CancellationException: ela e o mecanismo de cancelamento das coroutines. Captura-la impede o cancelamento correto.
  • Use withContext para trocar dispatchers: em vez de criar novos escopos, use withContext dentro de funções suspensas.
  • Prefira coroutineScope a GlobalScope: GlobalScope não respeita structured concurrency e pode causar leaks.
  • Exponha Flow em vez de suspend para streams: se os dados mudam ao longo do tempo, Flow e mais apropriado.
  • Teste com runTest: o kotlinx-coroutines-test fornece runTest para testar coroutines de forma deterministica.
  • Injete dispatchers: passe dispatchers como parametro para facilitar testes.

Erros Comuns e Armadilhas

  • Bloquear thread dentro de coroutine: chamar Thread.sleep() em vez de delay() bloqueia a thread da coroutine.
  • Criar coroutines sem escopo adequado: usar GlobalScope.launch e quase sempre um erro. Vincule ao ciclo de vida do componente.
  • Ignorar cancelamento: funções suspensas longas devem verificar isActive periodicamente ou usar funções suspensas cooperativas.
  • Misturar callbacks com coroutines: use suspendCancellableCoroutine para converter callbacks em funções suspensas corretamente.
  • Coleta duplicada de StateFlow: coletar o mesmo StateFlow em múltiplos locais sem necessidade pode causar processamento redundante.

Conclusão e Próximos Passos

Kotlin Coroutines oferecem uma abordagem poderosa e elegante para programação assíncrona. Com suspend functions, Flow, structured concurrency e dispatchers, você tem todas as ferramentas necessarias para construir aplicações responsivas e eficientes. Domine esses conceitos e aplique-os tanto no desenvolvimento Android quanto no backend com frameworks como Ktor e Spring Boot. Para comparar modelos de concorrência, veja como Go implementa goroutines, channels e pipelines e como Rust aborda async/await em profundidade. Explore os guias complementares sobre testes e arquitetura aqui no Kotlin Brasil para aprofundar ainda mais seu conhecimento.