Neste tutorial, você vai aprender os fundamentos de Coroutines em Kotlin — o mecanismo oficial da linguagem para programação assíncrona e concorrente. Coroutines permitem escrever código assíncrono de forma sequencial e legível, sem callbacks aninhados ou complexidade desnecessária. Ao final, você vai dominar suspend functions, launch, async/await, dispatchers, structured concurrency e os conceitos básicos de cancelamento.

O que São Coroutines?

Uma coroutine é uma instância de computação suspensível. Diferente de threads do sistema operacional, coroutines são extremamente leves — você pode criar milhares delas sem problemas de performance. Enquanto uma thread bloqueada consome recursos do sistema, uma coroutine suspensa libera a thread para fazer outro trabalho.

Pense em coroutines como funções que podem pausar sua execução em pontos específicos e retomar mais tarde, possivelmente em outra thread. Isso é perfeito para operações de I/O (rede, disco, banco de dados) que passam a maior parte do tempo esperando.

Configurando o Projeto

Antes de usar coroutines, você precisa adicionar a dependência no seu build.gradle.kts:

dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
    // Para Android, adicione também:
    // implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
}

Suspend Functions

Uma suspend function é uma função que pode ser pausada e retomada. Ela é marcada com a palavra-chave suspend:

import kotlinx.coroutines.*

suspend fun buscarDadosDoServidor(): String {
    delay(2000) // Simula uma chamada de rede (2 segundos)
    return "Dados carregados com sucesso!"
}

suspend fun salvarNoCache(dados: String) {
    delay(500) // Simula escrita no cache
    println("Cache atualizado: $dados")
}

A função delay() é o equivalente suspensível de Thread.sleep() — ela pausa a coroutine sem bloquear a thread. Suspend functions só podem ser chamadas de dentro de outras suspend functions ou de coroutine builders.

O importante é que suspend functions se parecem com código síncrono normal. Não há callbacks, não há .then(), não há observables encadeados. O código é lido de cima para baixo, como deveria ser.

runBlocking

O runBlocking é o ponto de entrada mais simples para o mundo das coroutines. Ele bloqueia a thread atual até que todas as coroutines internas terminem:

import kotlinx.coroutines.*

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

    val dados = buscarDadosDoServidor()
    println(dados)

    salvarNoCache(dados)

    println("Fim!")
}

// Saída:
// Início: main
// Dados carregados com sucesso!
// Cache atualizado: Dados carregados com sucesso!
// Fim!

Use runBlocking apenas em funções main() e em testes. Em código de produção (especialmente Android), você vai usar coroutine scopes apropriados.

launch: Coroutines Fire-and-Forget

O builder launch cria uma nova coroutine que roda de forma concorrente. Ele retorna um Job que pode ser usado para controlar a coroutine:

import kotlinx.coroutines.*

fun main() = runBlocking {
    println("Antes do launch")

    val job = launch {
        println("Coroutine iniciada")
        delay(1000)
        println("Coroutine finalizada")
    }

    println("Depois do launch — a coroutine está rodando em paralelo!")

    job.join() // Espera a coroutine terminar
    println("Tudo concluído")
}

// Saída:
// Antes do launch
// Depois do launch — a coroutine está rodando em paralelo!
// Coroutine iniciada
// Coroutine finalizada
// Tudo concluído

Veja como várias coroutines rodam concorrentemente:

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

    val job1 = launch {
        delay(1000)
        println("Tarefa 1 completa")
    }

    val job2 = launch {
        delay(1500)
        println("Tarefa 2 completa")
    }

    val job3 = launch {
        delay(800)
        println("Tarefa 3 completa")
    }

    // Todas rodam em paralelo!
    job1.join()
    job2.join()
    job3.join()

    val tempo = System.currentTimeMillis() - inicio
    println("Total: ${tempo}ms") // ~1500ms, NÃO 3300ms!
}

As três tarefas executam simultaneamente, então o tempo total é determinado pela tarefa mais lenta (1500ms), não pela soma de todas.

async/await: Coroutines com Retorno

Enquanto launch é para tarefas “fire-and-forget”, async é para quando você precisa de um resultado. Ele retorna um Deferred<T>, e você obtém o valor com await():

import kotlinx.coroutines.*

suspend fun buscarUsuario(): String {
    delay(1000)
    return "Ana Silva"
}

suspend fun buscarPedidos(): List<String> {
    delay(1500)
    return listOf("Pedido #1", "Pedido #2", "Pedido #3")
}

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

    // Executar as duas buscas em paralelo
    val usuarioDeferred = async { buscarUsuario() }
    val pedidosDeferred = async { buscarPedidos() }

    // Esperar os resultados
    val usuario = usuarioDeferred.await()
    val pedidos = pedidosDeferred.await()

    println("Usuário: $usuario")
    println("Pedidos: $pedidos")

    val tempo = System.currentTimeMillis() - inicio
    println("Total: ${tempo}ms") // ~1500ms (em paralelo)
}

Se as chamadas fossem sequenciais, levariam 2500ms (1000 + 1500). Com async, levam apenas ~1500ms porque rodam em paralelo. Isso é extremamente valioso em aplicações reais onde você precisa buscar dados de múltiplas fontes.

Coroutine Scope

Todo coroutine builder (launch, async) opera dentro de um scope. O scope define o ciclo de vida das coroutines:

import kotlinx.coroutines.*

fun main() = runBlocking {
    // runBlocking cria um CoroutineScope

    // coroutineScope cria um sub-scope que espera todas as coroutines filhas
    coroutineScope {
        launch {
            delay(500)
            println("Tarefa 1 do scope")
        }
        launch {
            delay(300)
            println("Tarefa 2 do scope")
        }
        println("Dentro do coroutineScope")
    }

    println("Depois do coroutineScope — todas as filhas terminaram")
}

// Em código real (como no Android):
class MinhaViewModel : ViewModel() {
    // viewModelScope é cancelado quando o ViewModel é destruído
    fun carregarDados() {
        viewModelScope.launch {
            val dados = buscarDadosDoServidor()
            // Atualizar UI
        }
    }
}

O coroutineScope é uma suspend function que cria um novo escopo e só retorna quando todas as coroutines filhas terminam. Se qualquer filha falhar, todas as outras são canceladas automaticamente.

Dispatchers

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

import kotlinx.coroutines.*

fun main() = runBlocking {
    // Dispatchers.Default — para trabalho intensivo de CPU
    launch(Dispatchers.Default) {
        println("Default: ${Thread.currentThread().name}")
        // Cálculos pesados, processamento de dados, algoritmos
        val resultado = (1..1_000_000).sum()
        println("Soma: $resultado")
    }

    // Dispatchers.IO — para operações de I/O
    launch(Dispatchers.IO) {
        println("IO: ${Thread.currentThread().name}")
        // Chamadas de rede, leitura/escrita de arquivos, banco de dados
        delay(100) // Simula I/O
    }

    // Dispatchers.Main — para atualizar a UI (Android)
    // launch(Dispatchers.Main) {
    //     textView.text = "Atualizado!"
    // }

    // Dispatchers.Unconfined — sem thread específica (raro de usar)
    launch(Dispatchers.Unconfined) {
        println("Unconfined: ${Thread.currentThread().name}")
    }
}

Na prática, o padrão mais comum é:

// Padrão Android típico
viewModelScope.launch {  // Main por padrão no Android
    val dados = withContext(Dispatchers.IO) {
        // Executar chamada de rede na thread de IO
        api.buscarDados()
    }
    // De volta na Main thread — seguro para atualizar UI
    atualizarTela(dados)
}

A função withContext() troca o dispatcher temporariamente e retorna ao dispatcher original quando termina.

Structured Concurrency

Structured concurrency é o princípio de que coroutines formam uma hierarquia pai-filho. Quando o scope pai é cancelado, todas as coroutines filhas são canceladas automaticamente:

import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = launch {
        // Coroutines filhas
        launch {
            repeat(10) { i ->
                println("Filha 1: iteração $i")
                delay(500)
            }
        }

        launch {
            repeat(10) { i ->
                println("Filha 2: iteração $i")
                delay(300)
            }
        }
    }

    delay(1200) // Deixa rodar por 1.2 segundos
    println("Cancelando o pai...")
    job.cancelAndJoin() // Cancela o pai E todas as filhas
    println("Todas as coroutines foram canceladas")
}

Isso garante que não existam coroutines “órfãs” rodando sem controle. É uma das vantagens mais importantes das coroutines sobre threads tradicionais.

Cancelamento de Coroutines

Coroutines suportam cancelamento cooperativo. Funções suspensíveis da stdlib (delay, yield, withContext) verificam automaticamente se a coroutine foi cancelada:

import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = launch {
        try {
            repeat(100) { i ->
                println("Processando item $i...")
                delay(200) // Ponto de cancelamento
            }
        } catch (e: CancellationException) {
            println("Coroutine cancelada! Limpando recursos...")
        } finally {
            println("Finally: cleanup executado")
        }
    }

    delay(1000)
    println("Solicitando cancelamento...")
    job.cancelAndJoin()
    println("Concluído")
}

Para código que faz computação intensiva sem chamar funções suspensíveis, use isActive ou ensureActive():

val job = launch(Dispatchers.Default) {
    var i = 0
    while (isActive) { // Verifica se a coroutine ainda está ativa
        i++
        // Trabalho intensivo de CPU
        if (i % 1000 == 0) println("Iteração $i")
    }
    println("Loop encerrado em $i")
}

delay(100)
job.cancelAndJoin()

Exemplo Prático Completo

Vamos simular um cenário real — carregar dados de múltiplas fontes em paralelo:

import kotlinx.coroutines.*

data class DashboardData(
    val usuario: String,
    val notificacoes: List<String>,
    val estatisticas: Map<String, Int>
)

suspend fun buscarPerfil(): String {
    delay(800)
    return "Diego Oliveira"
}

suspend fun buscarNotificacoes(): List<String> {
    delay(1200)
    return listOf("Nova mensagem", "Pedido enviado", "Promoção ativa")
}

suspend fun buscarEstatisticas(): Map<String, Int> {
    delay(1000)
    return mapOf("visitas" to 1500, "vendas" to 42, "avaliações" to 230)
}

suspend fun carregarDashboard(): DashboardData = coroutineScope {
    val perfilDeferred = async { buscarPerfil() }
    val notificacoesDeferred = async { buscarNotificacoes() }
    val estatisticasDeferred = async { buscarEstatisticas() }

    DashboardData(
        usuario = perfilDeferred.await(),
        notificacoes = notificacoesDeferred.await(),
        estatisticas = estatisticasDeferred.await()
    )
}

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

    val dashboard = carregarDashboard()

    println("Usuário: ${dashboard.usuario}")
    println("Notificações: ${dashboard.notificacoes}")
    println("Estatísticas: ${dashboard.estatisticas}")

    val tempo = System.currentTimeMillis() - inicio
    println("Carregado em ${tempo}ms") // ~1200ms (máximo das 3)
}

Três chamadas que somariam 3000ms sequencialmente executam em apenas ~1200ms em paralelo.

Erros Comuns

  1. Usar runBlocking em código de produção: runBlocking bloqueia a thread atual. Em Android, usá-lo na Main thread causará ANR (Application Not Responding). Use viewModelScope.launch ou lifecycleScope.launch em vez disso.

  2. Esquecer de tratar CancellationException: se você captura Exception genérica dentro de uma coroutine, pode acidentalmente engolir o CancellationException e impedir o cancelamento. Sempre relance CancellationException ou capture exceções mais específicas.

  3. Criar coroutines com GlobalScope: GlobalScope.launch cria coroutines sem structured concurrency — elas não são canceladas quando a tela muda ou o componente é destruído. Isso causa memory leaks. Sempre use scopes apropriados.

  4. Não usar o dispatcher correto: executar operações de I/O no Dispatchers.Main vai congelar a UI. Sempre use Dispatchers.IO para rede e disco, e Dispatchers.Default para computação pesada.

  5. Confundir launch com async: use launch quando não precisa de retorno (fire-and-forget) e async quando precisa de um resultado. Usar async sem nunca chamar await() pode mascarar exceções.

Conclusão e Próximos Passos

Coroutines são a forma idiomática de fazer programação assíncrona em Kotlin. Neste tutorial, você aprendeu suspend functions, launch e async/await, runBlocking, dispatchers, structured concurrency e cancelamento. Esses fundamentos são suficientes para começar a usar coroutines em projetos reais.

Para avançar no tema, explore:

  • Flow para streams de dados assíncronos
  • Channels para comunicação entre coroutines
  • Sealed Classes para modelar estados de resultado de operações assíncronas
  • Lambdas que são a base dos coroutine builders

A documentação oficial do Kotlin sobre coroutines é excelente e cobre cenários avançados como supervisão, exception handling e testing. Pratique refatorando callbacks existentes para coroutines e sinta a diferença na legibilidade do código.