O que é Coroutine em Kotlin?
Coroutines são o mecanismo do Kotlin para lidar com programação assíncrona de forma simples e eficiente. Em vez de usar callbacks aninhados ou threads pesadas, você escreve código assíncrono que parece sequencial – fácil de ler, fácil de manter.
Pense nas coroutines como “threads leves”. Você pode rodar milhares delas sem estourar a memória, porque elas não criam threads do sistema operacional – são gerenciadas pelo próprio Kotlin. O conceito é similar às goroutines de Go e ao async/await de Rust, cada linguagem com sua abordagem para concorrência leve.
Uma analogia útil: imagine um restaurante. Threads tradicionais seriam como ter um garçom exclusivo para cada mesa – caro e limitado. Coroutines funcionam como um garçom eficiente que atende várias mesas: enquanto espera o pedido de uma mesa ser preparado na cozinha, ele vai atender outra. O garçom (thread) nunca fica parado, é o restaurante funciona com poucos garçons atendendo muitas mesas.
Por que usar coroutines?
O problema clássico de código assíncrono é o tal do “callback hell”. Coroutines resolvem isso permitindo que você escreva código que parece síncrono, mas executa de forma assíncrona por baixo dos panos.
Exemplo básico
import kotlinx.coroutines.*
fun main() = runBlocking {
println("Início")
launch {
delay(1000)
println("Coroutine finalizada!")
}
println("Fazendo outras coisas...")
delay(1500)
println("Fim")
}
Saída:
Início
Fazendo outras coisas...
Coroutine finalizada!
Fim
O launch cria uma nova coroutine, e o delay suspende a execução sem bloquear a thread. Enquanto uma coroutine está esperando, outras podem rodar tranquilamente.
Conceitos fundamentais
- CoroutineScope: define o ciclo de vida das coroutines
- launch: inicia uma coroutine que não retorna valor
- async: inicia uma coroutine que retorna um resultado
- suspend: marca uma função que pode ser suspensa
- Dispatchers: controlam em qual thread a coroutine vai rodar
Exemplo com chamada de rede simulada
import kotlinx.coroutines.*
suspend fun buscarDados(): String {
delay(2000) // Simula uma chamada de rede
return "Dados carregados com sucesso"
}
fun main() = runBlocking {
println("Buscando dados...")
val resultado = buscarDados()
println(resultado)
}
Quando usar?
Coroutines são essenciais para operações de I/O (chamadas de rede, banco de dados, leitura de arquivos), processamento em background e qualquer tarefa que não deveria travar a interface do usuário. No Android, são praticamente obrigatórias hoje em dia.
Chamadas paralelas com async e await
Quando você precisa fazer várias operações ao mesmo tempo e combinar os resultados, async é a escolha certa:
import kotlinx.coroutines.*
suspend fun buscarUsuario(): String {
delay(1000)
return "Karina"
}
suspend fun buscarPedidos(): List<String> {
delay(1200)
return listOf("Pedido #1", "Pedido #2", "Pedido #3")
}
fun main() = runBlocking {
val inicio = System.currentTimeMillis()
val usuario = async { buscarUsuario() }
val pedidos = async { buscarPedidos() }
println("Usuário: ${usuario.await()}")
println("Pedidos: ${pedidos.await()}")
val tempo = System.currentTimeMillis() - inicio
println("Tempo total: ${tempo}ms") // ~1200ms, nao ~2200ms
}
As duas chamadas rodam em paralelo, e o tempo total é o da chamada mais lenta, não a soma das duas.
Tratamento de erros com coroutines
Lidar com exceções em coroutines exige atenção ao scope e ao job:
import kotlinx.coroutines.*
fun main() = runBlocking {
val handler = CoroutineExceptionHandler { _, excecao ->
println("Erro capturado: ${excecao.message}")
}
val scope = CoroutineScope(Dispatchers.Default + handler)
scope.launch {
println("Iniciando operacao...")
delay(500)
throw RuntimeException("Falha na conexão")
}
delay(1000) // Aguarda para ver o resultado
// Saída: Erro capturado: Falha na conexão
}
Coroutines com Flow para dados em sequência
Para fluxos de dados contínuos, combine coroutines com Flow:
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun monitorarTemperatura(): Flow<Double> = flow {
val sensores = listOf(22.5, 23.1, 24.0, 23.8, 25.2)
for (temp in sensores) {
delay(500) // Simula leitura do sensor
emit(temp)
}
}
fun main() = runBlocking {
monitorarTemperatura()
.filter { it > 23.0 }
.collect { temperatura ->
println("Alerta: temperatura em ${temperatura} graus")
}
}
Casos de Uso no Mundo Real
- Chamadas de API em Android: usar
viewModelScope.launchpara buscar dados de APIs REST sem travar a UI, substituindo AsyncTask e RxJava. - Processamento de arquivos: ler e processar arquivos grandes linha por linha usando coroutines com Dispatchers.IO para não bloquear a thread principal.
- Websockets e streaming: manter conexões ativas com servidores usando Flow e Channel para receber dados em tempo real.
- Operações em banco de dados: executar consultas Room ou Exposed em coroutines para manter a responsividade da aplicação.
- Tarefas periódicas: usar
while(isActive)comdelaypara executar verificações periódicas sem criar timers tradicionais.
Boas Práticas
- Nunca use
runBlockingem código de produção (exceto emmain()ou testes). Ele bloqueia a thread, anulando o propósito das coroutines. - Sempre defina um Dispatcher adequado: use
Dispatchers.IOpara operações de I/O,Dispatchers.Defaultpara CPU intensiva eDispatchers.Mainpara atualizações de UI. - Respeite a estrutura de concorrência (structured concurrency): lance coroutines dentro de um scope adequado para que sejam canceladas automaticamente quando o scope é destruído.
- Use
withContextpara trocar de dispatcher em vez de lançar uma nova coroutine. - Prefira Flow sobre Channel para fluxos de dados frios (que só produzem quando alguém consome).
Erros Comuns
- Esquecer de tratar exceções: exceções em
launchpropagam e cancelam o scope pai. Usetry-catchdentro da coroutine ou umCoroutineExceptionHandler. - Usar
GlobalScopeindiscriminadamente: coroutines lançadas emGlobalScopenão respeitam ciclo de vida e podem causar vazamento de memória. Use scopes vinculados ao componente (comoviewModelScopeno Android). - Chamar funções suspend sem estar em uma coroutine: funções suspend só podem ser chamadas de dentro de outra função suspend ou de um builder como
launchouasync. - Não cancelar coroutines desnecessárias: se o usuário saiu de uma tela, as coroutines daquela tela devem ser canceladas. Usar scopes vinculados ao ciclo de vida resolve isso automaticamente.
Perguntas Frequentes
Qual a diferença entre launch e async?
launch inicia uma coroutine que não retorna resultado (retorna um Job). async inicia uma coroutine que retorna um resultado encapsulado em um Deferred, acessível via await().
Coroutines substituem threads?
Não completamente. Coroutines rodam sobre threads, mas permitem multiplexar muitas tarefas em poucas threads. Para tarefas CPU-bound pesadas, você ainda precisa de threads adequadas via Dispatchers.Default.
Posso usar coroutines em projetos backend?
Sim. Frameworks como Ktor são construídos sobre coroutines. Spring Boot também oferece suporte completo a coroutines com suspend functions em controllers.
O que acontece se uma coroutine dentro de async lançar uma exceção?
A exceção é encapsulada no Deferred e será relançada quando você chamar await(). Use try-catch ao redor do await() ou um SupervisorScope para evitar que o erro cancele as coroutines irmãs.