Kotlin Coroutines transformaram a forma como lidamos com programacao assincrona e concorrente na JVM. Antes das coroutines, desenvolvedores precisavam lidar com callbacks aninhados, RxJava complexo ou threads manuais para executar operacoes assincronas. As coroutines oferecem uma abordagem sequencial para codigo assincrono, onde funcoes suspensas podem ser pausadas e retomadas sem bloquear threads. Este guia cobre desde os fundamentos ate padroes avancados que voce vai usar em projetos reais, tanto no Android quanto no backend.
O Que Sao Coroutines
Uma coroutine e uma instancia de computacao suspensavel. Diferente de threads, que sao gerenciadas pelo sistema operacional e consomem recursos significativos, coroutines sao leves e gerenciadas pelo runtime do Kotlin. Voce pode lancar milhares de coroutines sem impacto significativo no consumo de memoria, algo impraticavel com threads tradicionais.
import kotlinx.coroutines.*
fun main() = runBlocking {
launch {
delay(1000L)
println("Mundo!")
}
println("Ola,")
}
// Saida:
// Ola,
// Mundo!
O runBlocking cria um escopo de coroutine que bloqueia a thread atual ate que todas as coroutines filhas terminem. O launch inicia uma nova coroutine sem bloquear, e delay e uma funcao suspensa que pausa a coroutine sem bloquear a thread.
Suspend Functions
Funcoes marcadas com suspend podem ser pausadas e retomadas. Elas so podem ser chamadas de dentro de outras funcoes suspensas ou de um escopo de coroutine:
suspend fun buscarUsuario(id: Long): Usuario {
return withContext(Dispatchers.IO) {
// Simula chamada de rede
val resposta = apiService.buscarUsuario(id)
resposta.toDomain()
}
}
suspend fun buscarPedidos(usuarioId: Long): List<Pedido> {
return withContext(Dispatchers.IO) {
apiService.buscarPedidos(usuarioId).map { it.toDomain() }
}
}
// Chamadas sequenciais
suspend fun carregarDashboard(usuarioId: Long): Dashboard {
val usuario = buscarUsuario(usuarioId)
val pedidos = buscarPedidos(usuarioId)
return Dashboard(usuario, pedidos)
}
// Chamadas paralelas com async
suspend fun carregarDashboardParalelo(usuarioId: Long): Dashboard {
return coroutineScope {
val usuarioDeferred = async { buscarUsuario(usuarioId) }
val pedidosDeferred = async { buscarPedidos(usuarioId) }
Dashboard(usuarioDeferred.await(), pedidosDeferred.await())
}
}
A versao paralela com async e await executa ambas as chamadas simultaneamente, reduzindo o tempo total de espera.
Dispatchers
Os Dispatchers determinam em qual thread ou pool de threads a coroutine sera executada:
// Dispatchers.Main - Thread principal (UI no Android)
// Dispatchers.IO - Pool otimizado para I/O (rede, disco)
// Dispatchers.Default - Pool otimizado para CPU (calculos)
// Dispatchers.Unconfined - Sem thread especifica
suspend fun exemploDispatchers() {
// Operacao de rede
val dados = withContext(Dispatchers.IO) {
apiService.buscarDados()
}
// Processamento pesado
val resultado = withContext(Dispatchers.Default) {
dados.map { item ->
processarItem(item) // CPU intensivo
}
}
// Atualizar UI (Android)
withContext(Dispatchers.Main) {
exibirResultado(resultado)
}
}
O withContext troca o dispatcher dentro de uma funcao suspensa, permitindo executar cada operacao no contexto mais adequado.
Escopos de Coroutine
Cada coroutine pertence a um escopo que gerencia seu ciclo de vida. No Android, os escopos mais comuns sao viewModelScope e lifecycleScope:
class MinhaViewModel : ViewModel() {
fun carregarDados() {
// Cancelado automaticamente quando o ViewModel e destruido
viewModelScope.launch {
try {
val resultado = repository.buscarDados()
_estado.value = Estado.Sucesso(resultado)
} catch (e: CancellationException) {
throw e // Nunca engula CancellationException
} catch (e: Exception) {
_estado.value = Estado.Erro(e.message)
}
}
}
}
// Escopo personalizado
class MeuServico {
private val scope = CoroutineScope(
SupervisorJob() + Dispatchers.Default
)
fun iniciar() {
scope.launch {
// Trabalho em background
}
}
fun encerrar() {
scope.cancel() // Cancela todas as coroutines
}
}
Structured Concurrency
Structured concurrency garante que coroutines filhas sejam canceladas quando o escopo pai e cancelado, evitando leaks de coroutines:
suspend fun processarLote(itens: List<Item>) = coroutineScope {
val resultados = itens.map { item ->
async {
processarItem(item)
}
}
// Se qualquer async falhar, todas sao canceladas
resultados.awaitAll()
}
// Com SupervisorScope, falhas nao propagam para irmaos
suspend fun processarLoteIndependente(itens: List<Item>) = supervisorScope {
val resultados = itens.map { item ->
async {
try {
processarItem(item)
} catch (e: Exception) {
null // Falha individual nao cancela as demais
}
}
}
resultados.awaitAll().filterNotNull()
}
Kotlin Flow
Flow e a API para streams reativos das coroutines. Diferente do suspend, que retorna um unico valor, Flow emite multiplos valores ao longo do tempo:
// Criando um Flow
fun contadorFlow(): Flow<Int> = flow {
var contador = 0
while (true) {
emit(contador++)
delay(1000L)
}
}
// Operadores de transformacao
fun buscarProdutosFlow(query: String): Flow<List<Produto>> {
return flowOf(query)
.debounce(300L)
.filter { it.length >= 3 }
.flatMapLatest { consulta ->
repository.buscarProdutos(consulta)
}
.catch { e ->
emit(emptyList())
}
.flowOn(Dispatchers.IO)
}
// Coletando um Flow
viewModelScope.launch {
buscarProdutosFlow("kotlin")
.collect { produtos ->
_estado.value = Estado.Sucesso(produtos)
}
}
StateFlow e SharedFlow
StateFlow e SharedFlow sao versoes especializadas de Flow para compartilhar estado e eventos:
class ConfigViewModel : ViewModel() {
// StateFlow - sempre tem um valor atual
private val _tema = MutableStateFlow(Tema.CLARO)
val tema: StateFlow<Tema> = _tema.asStateFlow()
// SharedFlow - para eventos que nao precisam de valor inicial
private val _notificacoes = MutableSharedFlow<String>()
val notificacoes: SharedFlow<String> = _notificacoes.asSharedFlow()
fun alternarTema() {
_tema.value = if (_tema.value == Tema.CLARO) Tema.ESCURO else Tema.CLARO
}
fun enviarNotificacao(mensagem: String) {
viewModelScope.launch {
_notificacoes.emit(mensagem)
}
}
}
Tratamento de Erros
O tratamento de erros em coroutines exige atencao especial:
// CoroutineExceptionHandler para erros nao tratados
val handler = CoroutineExceptionHandler { _, exception ->
println("Erro capturado: ${exception.message}")
}
val scope = CoroutineScope(SupervisorJob() + handler)
// Try-catch em funcoes suspensas
suspend fun operacaoSegura(): Result<Dados> {
return try {
val dados = apiService.buscarDados()
Result.success(dados)
} catch (e: CancellationException) {
throw e // NUNCA capture CancellationException
} catch (e: HttpException) {
Result.failure(e)
} catch (e: IOException) {
Result.failure(e)
}
}
// Retry com backoff exponencial
suspend fun <T> retryComBackoff(
tentativas: Int = 3,
delayInicial: Long = 1000L,
fator: Double = 2.0,
bloco: suspend () -> T
): T {
var delayAtual = delayInicial
repeat(tentativas - 1) {
try {
return bloco()
} catch (e: Exception) {
delay(delayAtual)
delayAtual = (delayAtual * fator).toLong()
}
}
return bloco() // Ultima tentativa sem catch
}
Boas Praticas com Coroutines
- Nunca capture
CancellationException: ela e o mecanismo de cancelamento das coroutines. Captura-la impede o cancelamento correto. - Use
withContextpara trocar dispatchers: em vez de criar novos escopos, usewithContextdentro de funcoes suspensas. - Prefira
coroutineScopeaGlobalScope:GlobalScopenao respeita structured concurrency e pode causar leaks. - Exponha
Flowem vez desuspendpara streams: se os dados mudam ao longo do tempo, Flow e mais apropriado. - Teste com
runTest: okotlinx-coroutines-testfornecerunTestpara testar coroutines de forma deterministica. - Injete dispatchers: passe dispatchers como parametro para facilitar testes.
Erros Comuns e Armadilhas
- Bloquear thread dentro de coroutine: chamar
Thread.sleep()em vez dedelay()bloqueia a thread da coroutine. - Criar coroutines sem escopo adequado: usar
GlobalScope.launche quase sempre um erro. Vincule ao ciclo de vida do componente. - Ignorar cancelamento: funcoes suspensas longas devem verificar
isActiveperiodicamente ou usar funcoes suspensas cooperativas. - Misturar callbacks com coroutines: use
suspendCancellableCoroutinepara converter callbacks em funcoes suspensas corretamente. - Coleta duplicada de StateFlow: coletar o mesmo StateFlow em multiplos locais sem necessidade pode causar processamento redundante.
Conclusao e Proximos Passos
Kotlin Coroutines oferecem uma abordagem poderosa e elegante para programacao assincrona. Com suspend functions, Flow, structured concurrency e dispatchers, voce tem todas as ferramentas necessarias para construir aplicacoes responsivas e eficientes. Domine esses conceitos e aplique-os tanto no desenvolvimento Android quanto no backend com frameworks como Ktor e Spring Boot. Explore os guias complementares sobre testes e arquitetura aqui no Kotlin Brasil para aprofundar ainda mais seu conhecimento.