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
Usar
runBlockingem código de produção:runBlockingbloqueia a thread atual. Em Android, usá-lo na Main thread causará ANR (Application Not Responding). UseviewModelScope.launchoulifecycleScope.launchem vez disso.Esquecer de tratar
CancellationException: se você capturaExceptiongenérica dentro de uma coroutine, pode acidentalmente engolir oCancellationExceptione impedir o cancelamento. Sempre relanceCancellationExceptionou capture exceções mais específicas.Criar coroutines com
GlobalScope:GlobalScope.launchcria 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.Não usar o dispatcher correto: executar operações de I/O no
Dispatchers.Mainvai congelar a UI. Sempre useDispatchers.IOpara rede e disco, eDispatchers.Defaultpara computação pesada.Confundir
launchcomasync: uselaunchquando não precisa de retorno (fire-and-forget) easyncquando precisa de um resultado. Usarasyncsem nunca chamarawait()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.