Programmation asynchrone en Kotlin : guide complet pratique
Les applications modernes nécessitent des performances et une réactivité élevées. La programmation asynchrone en Kotlin avec les coroutines est devenue le standard de facto pour travailler avec les requêtes réseau, les bases de données et les opérations sur les fichiers. Dans ce guide, nous allons détailler les concepts fondamentaux, montrer des exemples pratiques et fournir des modèles prêts à l'emploi pour vos projets.
1. Bases des coroutines en Kotlin
Les coroutines sont des threads légers qui permettent de suspendre l'exécution d'une fonction sans bloquer le thread principal. Contrairement aux threads traditionnels, les coroutines peuvent être suspendues à tout moment et reprises plus tard, ce qui les rend idéales pour les opérations asynchrones.
1.1. Ajout des dépendances
Pour commencer, ajoutez les dépendances dans build.gradle.kts :
dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0") // pour Android}1.2. Première coroutine
Créons une coroutine simple avec runBlocking — un pont entre le monde synchrone et asynchrone :
import kotlinx.coroutines.*
fun main() = runBlocking { launch { delay(1000L) // délai non bloquant println("Monde !") } println("Bonjour,") delay(2000L) // attend la fin de la coroutine}Dans cet exemple, launch démarre une nouvelle coroutine, et delay est une fonction suspendue qui ne bloque pas le thread.
2. Fonctions suspend et parallélisme structuré
2.1. Création de fonctions suspend
Toute fonction pouvant être suspendue doit être marquée avec le modificateur suspend. Ces fonctions ne peuvent être appelées que depuis des coroutines ou d'autres fonctions suspend :
suspend fun fetchUserData(userId: String): User { delay(2000L) // simulation d'une requête réseau return User(userId, "Ivan Petrov")}
suspend fun fetchUserPosts(userId: String): List<Post> { delay(1500L) return listOf(Post("Titre 1"), Post("Titre 2"))}2.2. Exécution séquentielle
Par défaut, les coroutines s'exécutent séquentiellement à l'intérieur d'un même launch :
fun main() = runBlocking { val time = measureTimeMillis { val user = fetchUserData("123") val posts = fetchUserPosts("123") println("${user.name} a ${posts.size} publications") } println("Temps d'exécution : $time ms") // ~3500 ms}2.3. Exécution parallèle avec async/await
Pour une exécution parallèle, utilisez async et await :
fun main() = runBlocking { val time = measureTimeMillis { val user = async { fetchUserData("123") } val posts = async { fetchUserPosts("123") } println("${user.await().name} a ${posts.await().size} publications") } println("Temps d'exécution : $time ms") // ~2000 ms (en parallèle)}3. Dispatching des coroutines et contextes
Les dispatchers déterminent sur quel thread ou pool de threads la coroutine s'exécutera. Les dispatchers principaux :
- Dispatchers.Default — pour les tâches intensives en CPU (par défaut)
- Dispatchers.IO — pour les opérations d'entrée/sortie (réseau, fichiers)
- Dispatchers.Main — pour le thread UI (Android, Swing)
- Dispatchers.Unconfined — non lié à un thread spécifique
3.1. Exemple avec Dispatchers.IO
fun main() = runBlocking { launch(Dispatchers.IO) { // Requête réseau val response = httpClient.get("https://api.example.com/data") withContext(Di