Coroutines são, sem exagero, um dos recursos mais poderosos de Kotlin. Se você já sofreu com callbacks aninhados, threads manuais ou AsyncTask no Android, prepare-se: sua vida vai mudar. Vamos entender como funciona essa mágica.

O que são Coroutines?

Coroutines são uma forma de escrever código assíncrono de maneira sequencial. Em vez de usar callbacks ou encadear Promises, você escreve código que parece síncrono, mas por baixo dos panos executa de forma concorrente e não bloqueante.

Pense assim: uma coroutine é como uma função que pode ser pausada e retomada depois, sem bloquear a thread em que está rodando.

Configuração

Para usar coroutines, adicione a dependência no build.gradle.kts:

dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")
    // Para Android:
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0")
}

Primeiro exemplo: launch e delay

import kotlinx.coroutines.*

fun main() = runBlocking {
    println("Início - ${Thread.currentThread().name}")

    launch {
        delay(1000) // pausa sem bloquear a thread
        println("Coroutine 1 finalizou!")
    }

    launch {
        delay(500)
        println("Coroutine 2 finalizou!")
    }

    println("Coroutines disparadas!")
}

Saída:

Início - main
Coroutines disparadas!
Coroutine 2 finalizou!
Coroutine 1 finalizou!

Repare: as duas coroutines rodaram concorrentemente, e a coroutine 2 terminou primeiro porque tinha menos delay. Tudo isso sem criar threads extras.

Suspend Functions

O coração das coroutines são as suspend functions — funções que podem ser suspensas e retomadas:

suspend fun buscarDadosDoServidor(): String {
    delay(2000) // simula uma chamada de rede
    return "Dados recebidos com sucesso!"
}

suspend fun buscarDoBancoDeDados(): String {
    delay(1000) // simula consulta ao banco
    return "Dados do banco carregados!"
}

fun main() = runBlocking {
    val inicio = System.currentTimeMillis()

    // Execução sequencial
    val servidor = buscarDadosDoServidor()
    val banco = buscarDoBancoDeDados()
    println("$servidor | $banco")
    println("Tempo sequencial: ${System.currentTimeMillis() - inicio}ms") // ~3000ms
}

async e await: execução paralela

Quando as chamadas são independentes, use async para rodá-las em paralelo:

fun main() = runBlocking {
    val inicio = System.currentTimeMillis()

    // Execução paralela com async
    val servidorDeferred = async { buscarDadosDoServidor() }
    val bancoDeferred = async { buscarDoBancoDeDados() }

    val servidor = servidorDeferred.await()
    val banco = bancoDeferred.await()

    println("$servidor | $banco")
    println("Tempo paralelo: ${System.currentTimeMillis() - inicio}ms") // ~2000ms
}

A diferença é gritante: de 3 segundos caiu pra 2. O async dispara as duas operações ao mesmo tempo, e o await() espera o resultado.

Dispatchers: controlando onde a coroutine roda

Dispatchers determinam em qual thread (ou pool de threads) a coroutine será executada:

fun main() = runBlocking {
    // Thread principal
    launch(Dispatchers.Main) {
        // Atualizar UI (Android)
    }

    // Pool de threads otimizado para I/O
    launch(Dispatchers.IO) {
        // Chamadas de rede, leitura de arquivo, banco de dados
        val dados = fazerChamadaDeRede()
    }

    // Pool otimizado para CPU
    launch(Dispatchers.Default) {
        // Cálculos pesados, processamento de imagem
        val resultado = processarDadosPesados()
    }
}

No dia a dia:

  • Dispatchers.Main: atualizar interface (Android/Desktop)
  • Dispatchers.IO: operações de entrada/saída
  • Dispatchers.Default: trabalho intensivo de CPU

withContext: trocar de dispatcher

Muitas vezes você precisa trocar de contexto dentro da mesma coroutine:

class UsuarioRepository(private val api: ApiService) {

    suspend fun buscarUsuario(id: Int): Usuario {
        // Faz a chamada de rede em IO
        return withContext(Dispatchers.IO) {
            api.getUsuario(id)
        }
    }
}

// No ViewModel (Android):
class UsuarioViewModel : ViewModel() {
    fun carregar() {
        viewModelScope.launch { // roda em Main por padrão
            val usuario = repository.buscarUsuario(42) // muda pra IO internamente
            _uiState.value = UiState.Sucesso(usuario) // volta pra Main
        }
    }
}

Tratamento de erros

Coroutines oferecem tratamento de erros com try/catch convencional:

suspend fun operacaoArriscada(): String {
    delay(500)
    throw IllegalStateException("Deu ruim!")
}

fun main() = runBlocking {
    // Opção 1: try/catch simples
    try {
        val resultado = operacaoArriscada()
        println(resultado)
    } catch (e: Exception) {
        println("Erro capturado: ${e.message}")
    }

    // Opção 2: CoroutineExceptionHandler
    val handler = CoroutineExceptionHandler { _, exception ->
        println("Handler capturou: ${exception.message}")
    }

    val job = CoroutineScope(Dispatchers.Default + handler).launch {
        operacaoArriscada()
    }
    job.join()
}

Cancelamento de coroutines

Uma das belezas das coroutines é o cancelamento cooperativo:

fun main() = runBlocking {
    val job = launch {
        repeat(1000) { i ->
            println("Processando item $i...")
            delay(100) // ponto de cancelamento
        }
    }

    delay(500)
    println("Cansamos, vamos cancelar!")
    job.cancelAndJoin()
    println("Job cancelado com sucesso!")
}

Todas as suspend functions da biblioteca padrão (delay, yield, etc.) verificam o cancelamento automaticamente. Se você tem loops sem suspend functions, use isActive ou ensureActive().

Exemplo prático: buscar dados de várias APIs

Vamos a um cenário real — buscar dados de múltiplas fontes e combinar:

data class PaginaInicial(
    val noticias: List<Noticia>,
    val clima: Clima,
    val cotacaoDolar: Double
)

suspend fun carregarPaginaInicial(): PaginaInicial = coroutineScope {
    val noticias = async { noticiaService.buscarUltimas() }
    val clima = async { climaService.buscarPrevisao("São Paulo") }
    val cotacao = async { financeiroService.buscarCotacaoDolar() }

    PaginaInicial(
        noticias = noticias.await(),
        clima = clima.await(),
        cotacaoDolar = cotacao.await()
    )
}

As três chamadas rodam em paralelo e o resultado é combinado. Limpo, eficiente e fácil de entender.

Conclusão

Coroutines são o jeito Kotlin de lidar com concorrência: simples, seguro e poderoso. Uma vez que você pega o jeito, não vai querer voltar pra callbacks nunca mais. O segredo é praticar — comece substituindo chamadas assíncronas no seu projeto por coroutines e veja a diferença na legibilidade do código.

No próximo post, vamos falar sobre Kotlin Flow — o sistema reativo que se integra perfeitamente com coroutines. Até lá!