Kotlin Coroutines transformaram a forma como lidamos com programacao assincrona e concorrente na JVM. Antes das coroutines, desenvolvedores precisavam lidar com callbacks aninhados, RxJava complexo ou threads manuais para executar operacoes assincronas. As coroutines oferecem uma abordagem sequencial para codigo assincrono, onde funcoes suspensas podem ser pausadas e retomadas sem bloquear threads. Este guia cobre desde os fundamentos ate padroes avancados que voce vai usar em projetos reais, tanto no Android quanto no backend.

O Que Sao Coroutines

Uma coroutine e uma instancia de computacao suspensavel. Diferente de threads, que sao gerenciadas pelo sistema operacional e consomem recursos significativos, coroutines sao leves e gerenciadas pelo runtime do Kotlin. Voce pode lancar milhares de coroutines sem impacto significativo no consumo de memoria, 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 ate que todas as coroutines filhas terminem. O launch inicia uma nova coroutine sem bloquear, e delay e uma funcao suspensa que pausa a coroutine sem bloquear a thread.

Suspend Functions

Funcoes marcadas com suspend podem ser pausadas e retomadas. Elas so podem ser chamadas de dentro de outras funcoes 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 versao 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() {
    // Operacao 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 funcao suspensa, permitindo executar cada operacao 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 sao 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 sao 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 unico valor, Flow emite multiplos 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 sao versoes 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 Praticas 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 funcoes suspensas.
  • Prefira coroutineScope a GlobalScope: GlobalScope nao 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: funcoes suspensas longas devem verificar isActive periodicamente ou usar funcoes suspensas cooperativas.
  • Misturar callbacks com coroutines: use suspendCancellableCoroutine para converter callbacks em funcoes suspensas corretamente.
  • Coleta duplicada de StateFlow: coletar o mesmo StateFlow em multiplos locais sem necessidade pode causar processamento redundante.

Conclusao e Proximos Passos

Kotlin Coroutines oferecem uma abordagem poderosa e elegante para programacao assincrona. Com suspend functions, Flow, structured concurrency e dispatchers, voce tem todas as ferramentas necessarias para construir aplicacoes responsivas e eficientes. Domine esses conceitos e aplique-os tanto no desenvolvimento Android quanto no backend com frameworks como Ktor e Spring Boot. Explore os guias complementares sobre testes e arquitetura aqui no Kotlin Brasil para aprofundar ainda mais seu conhecimento.