O que são coroutines em Kotlin?
Se você já estudou um pouco de Kotlin, com certeza já ouviu falar em coroutines. Esse é um dos recursos mais poderosos da linguagem, mas também um dos que mais gera dúvida. Bora desmistificar?
Explicando de forma simples
Coroutines são uma forma de escrever código assíncrono de maneira sequencial e legível. Em vez de lidar com callbacks aninhados (o famoso “callback hell”) ou com APIs complicadas de threads, você escreve código que parece síncrono, mas que por baixo dos panos não bloqueia a thread principal.
Pense assim: imagine que você está num restaurante. Em vez de ficar parado esperando o prato ficar pronto (bloqueando), você faz o pedido e vai fazer outra coisa enquanto a cozinha trabalha. Quando o prato fica pronto, você volta pra comer. É isso que uma coroutine faz com seu código.
Um exemplo prático
import kotlinx.coroutines.*
fun main() = runBlocking {
println("Começando o programa...")
// Lança duas coroutines que rodam "ao mesmo tempo"
val pedido1 = async { buscarDados("Usuários", 2000) }
val pedido2 = async { buscarDados("Produtos", 1500) }
// Espera os dois resultados
println(pedido1.await())
println(pedido2.await())
println("Tudo pronto!")
}
suspend fun buscarDados(tipo: String, tempoMs: Long): String {
delay(tempoMs) // Simula uma operacao demorada (nao bloqueia a thread!)
return "Dados de $tipo carregados com sucesso"
}
Nesse exemplo, as duas buscas acontecem simultaneamente, e o tempo total é determinado pela mais lenta (2 segundos), e não pela soma das duas (3,5 segundos). Isso faz uma diferença enorme em aplicações reais.
Conceitos fundamentais
suspend fun: marca uma função que pode ser suspensa e retomada sem bloquear a thread. Confira o glossário de suspend para entender melhor.launch: inicia uma coroutine sem esperar o resultado (tipo “dispara e esquece”). Veja mais no glossário de launch.async/await: inicia uma coroutine e permite aguardar o resultado depois. Detalhes no glossário de async.runBlocking: cria um escopo de coroutine bloqueante, usado normalmente nomainou em testesdelay: pausa a coroutine sem bloquear a thread (diferente deThread.sleep)
Coroutines vs. Threads: qual a diferença?
Essa comparação ajuda muito a entender o valor das coroutines. Threads são gerenciadas pelo sistema operacional e consomem recursos significativos – cada thread precisa de sua própria pilha de memória (geralmente cerca de 1 MB). Já coroutines são gerenciadas pelo runtime do Kotlin e são extremamente leves.
Para ilustrar, veja a diferença na prática:
import kotlinx.coroutines.*
// Com coroutines: roda 100.000 sem problemas
fun main() = runBlocking {
val tarefas = List(100_000) {
launch {
delay(1000)
print(".")
}
}
tarefas.forEach { it.join() }
println("\nTodas as coroutines terminaram!")
}
Tentar criar 100.000 threads tradicionais provavelmente causaria um OutOfMemoryError. Já com coroutines, isso roda de boas porque elas compartilham um pool pequeno de threads por baixo dos panos.
Outra diferença importante: threads usam preemptive multitasking (o sistema operacional decide quando pausar), enquanto coroutines usam cooperative multitasking (a coroutine decide quando ceder o controle, nos pontos de suspensão). Isso torna o comportamento mais previsível.
Dispatchers: onde a coroutine executa
Os dispatchers definem em qual thread ou pool de threads a coroutine vai rodar. Escolher o dispatcher correto é essencial:
import kotlinx.coroutines.*
suspend fun exemploDispatchers() {
// Para operacoes de CPU (processamento pesado)
withContext(Dispatchers.Default) {
// Calculos complexos, ordenacao, etc.
}
// Para operacoes de I/O (rede, banco de dados, arquivos)
withContext(Dispatchers.IO) {
// Chamadas de API, leitura de arquivos, queries no banco
}
// Para atualizar a UI (Android)
withContext(Dispatchers.Main) {
// Atualizar textos, mostrar dados na tela
}
}
Usar Dispatchers.IO para chamadas de rede e Dispatchers.Default para processamento pesado evita que a thread principal fique bloqueada. No Android, isso é obrigatório para manter o app responsivo.
Quando usar coroutines?
Coroutines brilham em cenários como:
- Chamadas de API: buscar dados de um servidor sem congelar a interface do app
- Acesso a banco de dados: consultas ao Room ou outros bancos locais
- Operações paralelas: quando você precisa fazer várias coisas ao mesmo tempo e combinar os resultados
- Tarefas em background: processamento de imagens, sincronização de dados, uploads
- Streams de dados: combinadas com Flow, permitem trabalhar com fluxos de dados reativos. Veja nosso tutorial de Kotlin Flow e o guia completo de coroutines.
Structured Concurrency: coroutines organizadas
Um conceito central em Kotlin é a concorrência estruturada. Toda coroutine precisa ser lançada dentro de um escopo (CoroutineScope), e esse escopo controla o ciclo de vida de todas as coroutines filhas. Se o escopo for cancelado, todas as coroutines dentro dele também são canceladas automaticamente.
import kotlinx.coroutines.*
fun main() = runBlocking {
val job = launch {
launch { tarefaA() }
launch { tarefaB() }
// Se esse escopo for cancelado, tarefaA e tarefaB tambem sao
}
delay(500)
job.cancel() // Cancela tudo de uma vez
println("Tarefas canceladas")
}
suspend fun tarefaA() {
repeat(10) {
delay(200)
println("Tarefa A: passo $it")
}
}
suspend fun tarefaB() {
repeat(10) {
delay(300)
println("Tarefa B: passo $it")
}
}
Para entender melhor como escopos e jobs funcionam juntos, confira o tutorial avançado de coroutines.
Por que coroutines são tão importantes?
No desenvolvimento Android, a thread principal (UI thread) não pode ser bloqueada, senão o app trava. Coroutines resolvem isso de forma elegante, permitindo que operações pesadas como chamadas de API e acesso a banco de dados rodem sem congelar a interface. No Android, os escopos mais usados são viewModelScope e lifecycleScope, que se integram com o ViewModel e o ciclo de vida dos componentes.
No backend, coroutines permitem que um servidor atenda milhares de requisições simultâneas com muito menos recursos do que seria necessário usando threads tradicionais. Frameworks como Ktor foram construídos em cima de coroutines, e até o Spring Boot com Kotlin oferece suporte nativo.
O principal cuidado
Coroutines são leves, mas não são mágicas. É importante entender os escopos e o ciclo de vida de cada coroutine pra evitar vazamentos de memória e comportamentos inesperados. No Android, por exemplo, use viewModelScope ou lifecycleScope em vez de criar escopos soltos.
Outro cuidado comum: nunca use runBlocking dentro de uma coroutine. Ele bloqueia a thread atual e pode causar deadlocks. Reserve o runBlocking para o main() e para testes unitários.
Dominar coroutines leva um tempinho, mas é um investimento que vale muito a pena. É um daqueles conhecimentos que transformam a forma como você programa. Para ir mais fundo, comece pelo tutorial básico de coroutines e depois avance para o guia completo.