launch/join vs async/await em Kotlin: o que escolher em 2025? Análise completa com exemplos
\n\nOlá, colega! Se você já escreveu coroutines em Kotlin, certamente já se perguntou: "Qual é, afinal, a diferença entre launch e async? E para que serve esse misterioso join?". Essa pergunta é um verdadeiro sucesso no Stack Overflow, com milhares de visualizações. E não é à toa: à primeira vista, ambas as ferramentas fazem a mesma coisa — iniciam uma tarefa em segundo plano. Mas o diabo, como sempre, está nos detalhes.
Em 2025, quando Kotlin coroutines se tornaram mainstream no desenvolvimento Android e no backend, entender essas diferenças não é apenas um capricho, mas uma necessidade para escrever código limpo, seguro e eficiente. Neste artigo, não vamos apenas listar fatos secos. Vamos analisar cenários reais, olhar para o código e extrair as melhores práticas que você pode aplicar com segurança em seus projetos agora mesmo.
\n\nVamos nessa!
\n\nlaunch: "Iniciou e esqueceu"
\n\nA função launch é, provavelmente, a maneira mais simples de iniciar uma coroutine. Ela cria uma nova coroutine que não retorna um resultado. Sua tarefa é executar algum trabalho: atualizar a UI, enviar um log, gravar dados em um arquivo. O tipo do valor retornado é Job. É como dar uma tarefa a um entregador: "Leve o pacote e volte, não preciso de relatório".
O método join permite aguardar a conclusão dessa coroutine. Ele suspende a execução da coroutine atual até que o job que chamou join seja concluído.
Vamos considerar um exemplo. Suponha que precisamos carregar duas imagens em paralelo e depois combiná-las. Usando launch e join, ficaria assim:
import kotlinx.coroutines.*\n\nsuspend fun loadImage(url: String): Image {\n // simulação de carregamento\n delay(1000)\n return Image(url)\n}\n\ndata class Image(val url: String)\n\nsuspend fun loadTwoImagesWithLaunch() = coroutineScope {\n var image1: Image? = null\n var image2: Image? = null\n\n val job1 = launch {\n image1 = loadImage("https://example.com/img1.png")\n }\n\n val job2 = launch {\n image2 = loadImage("https://example.com/img2.png")\n }\n\n // Aguardamos ambas as tarefas\n job1.join()\n job2.join()\n\n // Agora image1 e image2 estão garantidamente carregadas\n println("Images loaded: $image1 and $image2")\n}\n\nO que é importante aqui? Usamos variáveis mutáveis (var) para armazenar o resultado. Isso não é muito elegante e pode levar a problemas com threads, embora as coroutines operem dentro de uma mesma thread. Mas a principal desvantagem é que não podemos usar launch para retornar um valor diretamente.
async: "Iniciou e obtenha o resultado"
\n\nJá o async é a "classe executiva". Ele cria uma coroutine que retorna um resultado na forma de um objeto Deferred<T>. É como pedir um táxi: você espera, e recebe não apenas o fato da viagem, mas um resultado concreto — você chegou ao destino. Para obter esse resultado, usa-se await().
O mesmo exemplo de carregamento de imagens, mas usando async/await, fica mais elegante:
import kotlinx.coroutines.*\n\nsuspend fun loadImage(url: String): Image {\n delay(1000)\n return Image(url)\n}\n\ndata class Image(val url: String)\n\nsuspend fun loadTwoImagesWithAsync() = coroutineScope {\n val deferred1 = async { loadImage("https://example.com/img1.png") }\n val deferred2 = async { loadImage("https://example.com/img2.png") }\n\n // await() suspende a coroutine até obter o resultado\n val image1 = deferred1.await()\n val image2 = deferred2.await()\n\n println("Images loaded: $image1 and $image2")\n}\n\nSente a diferença? Nada de var, nada de gerenciamento manual. Tudo limpo,