Kotlin avançado: dominando Coroutines para concorrência eficaz
Rocketseat

Rocketseat

4 min de leitura
Kotlin
Fala, Dev! Já se aventurou pelo mundo Kotlin e está familiarizado com as funções suspend e o launch? Ótimo! Mas, à medida que seus projetos crescem, você provavelmente se pega buscando soluções mais avançadas para lidar com a concorrência. É aí que entram as Coroutines, uma ferramenta poderosa que pode simplificar (e muito!) a sua vida.
Neste artigo, vamos desmistificar as Coroutines, desde os fundamentos até os truques avançados. Nosso objetivo? Transformar você em um mestre da concorrência em Kotlin, capaz de criar aplicativos mais rápidos, responsivos e eficientes. E o melhor: de um jeito que você vai gostar de aprender!

Por que Coroutines? O problema da concorrência tradicional

Imagine que você está em um restaurante. Você faz o pedido (uma requisição à rede), mas, em vez de esperar pacientemente, você fica parado na frente do garçom, bloqueando todos os outros clientes (sua thread principal!), até que sua comida chegue. Nada eficiente, certo?
No mundo do desenvolvimento, essa "espera bloqueante" é um problema real. Threads são recursos caros, e mantê-las ocupadas esperando por operações demoradas (como chamadas de rede ou leitura de arquivos) é um desperdício. As abordagens tradicionais, como callbacks, podem até resolver o problema, mas transformam seu código em um emaranhado difícil de entender e manter.
As Coroutines entram em cena como uma solução elegante e eficiente. Elas permitem que você escreva código assíncrono de forma sequencial, como se estivesse escrevendo código síncrono normal, mas sem bloquear threads. É como se o garçom do nosso exemplo pudesse anotar seu pedido e continuar atendendo outros clientes enquanto sua comida é preparada!

Mergulhando nos fundamentos das Coroutines:

Vamos começar com os blocos de construção essenciais:

1. Funções suspend: a magia por trás das coroutines

suspend fun buscarDadosDaRede(): String { // ... código para fazer a requisição ... delay(1000) // Simulando uma operação demorada (não bloqueia a thread!) return "Dados recebidos!" }
A palavra-chave suspend é o que torna uma função "suspensível". Isso significa que ela pode pausar sua execução sem bloquear a thread, permitindo que outras tarefas sejam executadas. Quando a operação demorada (como o delay acima) for concluída, a função retoma de onde parou.
O que acontece nos bastidores? O compilador Kotlin faz um truque de mágica, transformando sua função suspend em uma máquina de estados. Ele salva o estado da função quando ela precisa esperar e a restaura quando a operação é concluída. Você não precisa se preocupar com isso, mas é bom saber que existe uma engenharia inteligente por trás!

2. CoroutineScope: maestro da orquestra

Para iniciar uma Coroutine, você precisa de um CoroutineScope. Pense nele como o maestro de uma orquestra, responsável por gerenciar o ciclo de vida das Coroutines.
val meuEscopo = CoroutineScope(Dispatchers.Main) // Cria um escopo ligado à thread principal meuEscopo.launch { // Inicia uma nova Coroutine val dados = buscarDadosDaRede() println(dados) // Será executado depois que buscarDadosDaRede() terminar } // O código aqui continua executando *imediatamente*, sem esperar a Coroutine terminar!
No Android, você geralmente usará viewModelScope ou lifecycleScope, que são escopos pré-definidos vinculados ao ciclo de vida de um ViewModel ou de um componente do ciclo de vida (como uma Activity ou Fragment). Isso garante que suas Coroutines sejam canceladas automaticamente quando o ViewModel ou componente for destruído, evitando vazamentos de memória.

3. CoroutineContext: os detalhes importantes

O CoroutineContext é um conjunto de elementos que definem o comportamento de uma Coroutine. Os mais importantes são:
  • Job: que controla o ciclo de vida da Coroutine. Permite que você cancele a Coroutine, verifique seu estado (ativa, cancelada, etc.) e aguarde sua conclusão.
  • CoroutineDispatcher: que determina em qual thread (ou pool de threads) a Coroutine será executada.

4. CoroutineDispatcher: escolhendo a thread certa

launch(Dispatchers.IO) { // Executa em uma thread otimizada para operações de I/O val arquivo = lerArquivoDoDisco() // ... } launch(Dispatchers.Main) { // Executa na thread principal (UI) atualizarInterface(dados) } launch(Dispatchers.Default) { // Executa em uma thread otimizada para uso intensivo de CPU val resultado = calcularPi(1000000) //Calcula PI com 1 milhão de casas decimais } // Evite Dispatchers.Unconfined em produção! (A menos que você saiba *exatamente* o que está fazendo)
Limitar paralelismo:
withContext(Dispatchers.Default.limitedParallelism(3)) { //Operação que será executada utilizando no máximo 3 threads }
Escolher o Dispatcher correto é crucial para o desempenho do seu aplicativo. Use Dispatchers.IO para operações de rede, leitura/escrita de arquivos, etc. Use Dispatchers.Main para atualizar a interface do usuário. Use Dispatchers.Default para cálculos intensivos que usam muita CPU.

5. launch e async: iniciando coroutines

  • launch: inicia uma Coroutine no estilo "dispare e esqueça" (fire-and-forget). Não retorna nenhum resultado.
  • async: inicia uma Coroutine que retorna um resultado (um objeto Deferred).
val job = launch { // Inicia uma Coroutine, mas não retorna um resultado // ... } val resultado: Deferred<Int> = async { // Inicia uma Coroutine que retorna um Int // ... return 42 } val valor = resultado.await() // Espera (suspende) até que o resultado esteja pronto
await() é uma função suspensa que espera pelo resultado de uma Coroutine iniciada com async. Ela não bloqueia a thread; em vez disso, suspende a Coroutine atual até que o resultado esteja disponível.

Indo Além: Coroutines em ação

Agora que você conhece os fundamentos, vamos ver como usar Coroutines em cenários mais complexos.

Execução sequencial vs. paralela

// Sequencial: suspend fun fazerTarefa1(): String { /* ... */ } suspend fun fazerTarefa2(): Int { /* ... */ } suspend fun executarSequencialmente() { val resultado1 = fazerTarefa1() // Espera fazerTarefa1() terminar val resultado2 = fazerTarefa2() // Espera fazerTarefa2() terminar println("$resultado1 - $resultado2") } // Paralela: suspend fun executarParalelamente() = coroutineScope { val resultado1 = async { fazerTarefa1() } // Inicia fazerTarefa1() em paralelo val resultado2 = async { fazerTarefa2() } // Inicia fazerTarefa2() em paralelo println("${resultado1.await()} - ${resultado2.await()}") // Espera *ambas* terminarem }
Usando async e await, você pode executar várias tarefas em paralelo e esperar que todas terminem antes de prosseguir. Isso pode acelerar significativamente seu aplicativo!

withContext: mudando de Dispatcher

suspend fun buscarDados() = withContext(Dispatchers.IO) { // Faz uma requisição à rede (operação de I/O) // ... }
withContext permite que você altere o Dispatcher de um bloco de código dentro de uma Coroutine. É útil quando você precisa realizar uma operação específica em uma thread diferente.

coroutineScope vs. supervisorScope

  • coroutineScope: cria um novo escopo para Coroutines. Se uma das Coroutines filhas falhar, todas as outras serão canceladas.
  • supervisorScope: cria um escopo que não cancela as outras Coroutines filhas se uma delas falhar. Use com cuidado!
suspend fun processarDados() = supervisorScope { val tarefa1 = launch { /* ... */ } // Se falhar, tarefa2 não será cancelada val tarefa2 = launch { /* ... */ } }

withTimeout e withTimeoutOrNull

val resultado = withTimeoutOrNull(5000) { // Define um tempo limite de 5 segundos fazerOperacaoDemorada() } if (resultado == null) { // A operação demorou mais de 5 segundos e foi cancelada }
Adicione um tempo limite para suas coroutines, para não deixar o usuário esperando indefinidamente.

Lidando com exceções

Exceções em Coroutines se propagam de forma hierárquica. Se uma Coroutine filha lançar uma exceção não tratada, ela cancelará sua Coroutine pai e, potencialmente, todas as outras Coroutines no mesmo escopo (a menos que você use um SupervisorJob).

try-catch

Você pode usar blocos try-catch normalmente dentro de Coroutines:
try { val resultado = fazerOperacaoArriscada() // ... } catch (e: IOException) { // Lidar com a exceção }

CoroutineExceptionHandler

Para lidar com exceções não tratadas de forma global, você pode usar um CoroutineExceptionHandler:
val handler = CoroutineExceptionHandler { _, exception -> println("Exceção capturada: $exception") // Fazer algo (por exemplo, exibir uma mensagem de erro) } val meuEscopo = CoroutineScope(Dispatchers.Main + handler) // Adiciona o handler ao escopo meuEscopo.launch { // ... código que pode lançar exceções ... }

SupervisorJob

Para evitar que a falha de uma Coroutine cancele outras, use um SupervisorJob:
val meuEscopo = CoroutineScope(SupervisorJob() + Dispatchers.Main) meuEscopo.launch { /* ... */ } // Se esta Coroutine falhar, as outras não serão canceladas meuEscopo.launch { /* ... */ }
Se uma coroutine filha falhar, irá cancelar apenas ela.

Boas práticas e erros comuns

  • Nunca use GlobalScope: ele cria Coroutines que não estão vinculadas a nenhum ciclo de vida e podem causar vazamentos de memória. Use viewModelScope, lifecycleScope ou crie seus próprios escopos.
  • Não passe CoroutineScope como argumento: em vez disso, use funções suspensas e coroutineScope.
  • Evite re-dispatching desnecessário: se você já estiver na thread correta (por exemplo, Dispatchers.Main), não use withContext(Dispatchers.Main) novamente. Use Dispatchers.Main.immediate para evitar o custo de mudar de thread se já estiver na thread principal.
  • Cancele Coroutines quando não forem mais necessárias: use Job.cancel() para cancelar Coroutines explicitamente ou use escopos vinculados ao ciclo de vida para que elas sejam canceladas automaticamente.
  • Use Mutex e AtomicReference para evitar condições de corrida em código multi-thread.
🚀
E aí, curtiu essa imersão no mundo das Coroutines? Este é apenas o começo! Compartilhe suas experiências, dúvidas e dicas em nossa comunidade.

Próximos passos

  • Comece a praticar! Refatore o código do seu projeto.
  • Explore tópicos avançados: Canais (Channels)
Artigos_

Explore conteúdos relacionados

Descubra mais artigos que complementam seu aprendizado e expandem seu conhecimento.

Aprenda programação do zero e DE GRAÇA

No Discover você vai descomplicar a programação, aprender a criar seu primeiro site com a mão na massa e iniciar sua transição de carreira.

COMECE A ESTUDAR AGORA