launch/join vs async/await dans Kotlin : que choisir en 2025 ? Analyse complète avec exemples
\n\nBonjour, collègue ! Si tu as déjà écrit des coroutines en Kotlin, tu t'es sûrement demandé : « Quelle est la différence entre launch et async ? Et à quoi sert ce mystérieux join ? ». Cette question est un véritable hit sur Stack Overflow, avec des milliers de vues. Et ce n'est pas sans raison : à première vue, les deux outils font la même chose – lancer une tâche en arrière-plan. Mais le diable, comme toujours, se cache dans les détails.
En 2025, alors que les coroutines Kotlin sont devenues un standard dans le développement Android et le backend, comprendre ces différences n'est pas un simple caprice, mais une nécessité pour écrire un code propre, sûr et performant. Dans cet article, nous ne nous contenterons pas d'énumérer des faits secs. Nous analyserons des scénarios réels, examinerons le code et dégagerons les meilleures pratiques que vous pouvez appliquer dès maintenant dans vos projets.
\n\nC'est parti !
\n\nlaunch : « Lance et oublie »
\n\nLa fonction launch est sans doute la façon la plus simple de lancer une coroutine. Elle crée une nouvelle coroutine qui ne retourne pas de résultat. Sa mission est d'effectuer un travail : mettre à jour l'interface utilisateur, envoyer un log, écrire des données dans un fichier. Le type de retour est Job. C'est comme donner une tâche à un livreur : « Livre le colis et reviens, aucun rapport n'est nécessaire ».
La méthode join permet d'attendre la fin de cette coroutine. Elle suspend l'exécution de la coroutine actuelle jusqu'à ce que le job ayant appelé join soit terminé.
Prenons un exemple. Supposons que nous devions charger deux images en parallèle, puis les combiner. Avec launch et join, cela ressemblerait à ceci :
import kotlinx.coroutines.*\n\nsuspend fun loadImage(url: String): Image {\n // simulation de chargement\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 // On attend les deux tâches\n job1.join()\n job2.join()\n\n // Maintenant, image1 et image2 sont garantis chargés\n println("Images chargées : $image1 et $image2")\n}\n\nQu'est-ce qui est important ici ? Nous utilisons des variables mutables (var) pour stocker le résultat. Ce n'est pas très élégant et peut entraîner des problèmes de threads, même si les coroutines fonctionnent dans un seul thread. Mais le principal inconvénient est que nous ne pouvons pas utiliser launch pour retourner une valeur directement.
async : « Lance et obtiens un résultat »
\n\nQuant à async, c'est la « classe affaires ». Il crée une coroutine qui retourne un résultat sous la forme d'un objet Deferred<T>. C'est comme commander un taxi : vous attendez, et on vous retourne non pas le simple fait du trajet, mais un résultat concret – vous êtes arrivé. Pour obtenir ce résultat, on utilise await().
Le même exemple de chargement d'images, mais avec async/await, devient plus élégant :
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() suspend la coroutine jusqu'à l'obtention du résultat\n val image1 = deferred1.await()\n val image2 = deferred2.await()\n\n println("Images chargées : $image1 et $image2")\n}\n\nVous sentez la différence ? Pas de var, pas de gestion manuelle. Tout est propre,