Passer au contenu principal

⏸️ Fonctions suspend

📖 Qu'est-ce que suspend ?

Les fonctions suspend sont des fonctions spéciales qui peuvent suspendre une coroutine. Elles sont efficaces car elles permettent d'attendre sans bloquer le thread !

💡 Concepts de base

Le mot-clé suspend

// Une fonction suspend peut appeler d'autres fonctions suspend
suspend fun doSomething() {
delay(1000) // fonction suspend
println("Terminé !")
}

fun main() = runBlocking {
doSomething() // Appel dans une coroutine
}

Fonction normale vs fonction suspend

// ❌ Impossible d'appeler une fonction suspend depuis une fonction normale
fun normalFunction() {
// delay(1000) // Erreur de compilation !
}

// ✅ Possible dans une fonction suspend
suspend fun suspendFunction() {
delay(1000) // OK !
}

🎯 Exemples pratiques

Requête réseau

data class User(val id: String, val name: String)

suspend fun fetchUser(userId: String): User {
delay(1000) // Simulation d'un délai réseau
return User(userId, "홍길동")
}

fun main() = runBlocking {
println("Début de la requête")
val user = fetchUser("user123")
println("Utilisateur reçu : ${user.name}")
}

Exécution séquentielle

suspend fun prepareFood(): String {
delay(1000)
return "음식"
}

suspend fun setTable(): String {
delay(500)
return "테이블"
}

suspend fun prepareMeal() {
println("Début de la préparation")

val food = prepareFood()
println("$food préparé")

val table = setTable()
println("$table préparé")

println("Toute la préparation est terminée !")
}

fun main() = runBlocking {
val time = measureTimeMillis {
prepareMeal()
}
println("Temps écoulé : ${time}ms") // ~1500ms
}

Exécution parallèle

suspend fun prepareMealParallel() = coroutineScope {
println("Début de la préparation")

val foodDeferred = async { prepareFood() }
val tableDeferred = async { setTable() }

val food = foodDeferred.await()
val table = tableDeferred.await()

println("$food, $table préparés")
}

fun main() = runBlocking {
val time = measureTimeMillis {
prepareMealParallel()
}
println("Temps écoulé : ${time}ms") // ~1000ms (parallèle !)
}

🔧 withContext

Changement de dispatcher

suspend fun loadData(): String {
return withContext(Dispatchers.IO) {
// Exécution sur le thread IO
delay(1000)
"데이터"
}
}

fun main() = runBlocking {
println("Thread principal : ${Thread.currentThread().name}")

val data = loadData()

println("Retour au principal : ${Thread.currentThread().name}")
println("Données : $data")
}

Plusieurs contextes

suspend fun processData() {
// Opération IO
val data = withContext(Dispatchers.IO) {
readFromFile()
}

// Calcul intensif CPU
val processed = withContext(Dispatchers.Default) {
heavyComputation(data)
}

// Thread principal (mise à jour UI, etc.)
withContext(Dispatchers.Main) {
updateUI(processed)
}
}

suspend fun readFromFile(): String {
delay(500)
return "파일 내용"
}

suspend fun heavyComputation(data: String): String {
delay(1000)
return data.uppercase()
}

suspend fun updateUI(data: String) {
println("Mise à jour UI : $data")
}

🎨 Patterns avancés

Attente conditionnelle

suspend fun fetchDataIfNeeded(cache: String?): String {
return cache ?: fetchFromNetwork()
}

suspend fun fetchFromNetwork(): String {
delay(1000)
return "새 데이터"
}

fun main() = runBlocking {
// Avec cache - retour immédiat
val cached = fetchDataIfNeeded("캐시 데이터")
println(cached)

// Sans cache - requête réseau
val fresh = fetchDataIfNeeded(null)
println(fresh)
}

Chaînage

suspend fun step1(): Int {
delay(500)
return 10
}

suspend fun step2(input: Int): Int {
delay(500)
return input * 2
}

suspend fun step3(input: Int): Int {
delay(500)
return input + 5
}

suspend fun processChain(): Int {
val a = step1()
val b = step2(a)
val c = step3(b)
return c
}

fun main() = runBlocking {
val result = processChain()
println("Résultat final : $result") // 25 (10 * 2 + 5)
}

Gestion des erreurs

suspend fun riskyOperation(): String {
delay(1000)
throw Exception("Erreur réseau !")
}

suspend fun safeOperation(): String? {
return try {
riskyOperation()
} catch (e: Exception) {
println("Erreur survenue : ${e.message}")
null
}
}

fun main() = runBlocking {
val result = safeOperation()
println("Résultat : ${result ?: "Échec"}")
}

🔥 Fonctions utiles

Gestion du timeout

suspend fun fetchWithTimeout(): String? {
return try {
withTimeout(2000) {
delay(3000) // Prend trop de temps
"데이터"
}
} catch (e: TimeoutCancellationException) {
println("Temps dépassé !")
null
}
}

fun main() = runBlocking {
val result = fetchWithTimeout()
println("Résultat : ${result ?: "Timeout"}")
}

Logique de retry

suspend fun <T> retry(
times: Int = 3,
initialDelay: Long = 100,
factor: Double = 2.0,
block: suspend () -> T
): T {
var currentDelay = initialDelay
repeat(times - 1) { attempt ->
try {
return block()
} catch (e: Exception) {
println("Tentative ${attempt + 1} échouée : ${e.message}")
}
delay(currentDelay)
currentDelay = (currentDelay * factor).toLong()
}
return block() // Dernière tentative
}

var attemptCount = 0

suspend fun unstableAPI(): String {
attemptCount++
if (attemptCount < 3) {
throw Exception("Erreur temporaire")
}
return "Succès !"
}

fun main() = runBlocking {
val result = retry(times = 5) {
unstableAPI()
}
println(result)
}

Pattern de cache

class DataRepository {
private var cache: String? = null

suspend fun getData(forceRefresh: Boolean = false): String {
if (!forceRefresh && cache != null) {
println("Retour depuis le cache")
return cache!!
}

println("Récupération depuis le réseau...")
delay(1000)
val data = "새 데이터"
cache = data
return data
}
}

fun main() = runBlocking {
val repo = DataRepository()

// Premier appel - réseau
println(repo.getData())

// Deuxième appel - cache
println(repo.getData())

// Rafraîchissement forcé
println(repo.getData(forceRefresh = true))
}

🛡️ Fonctions annulables

Vérification d'annulation

suspend fun longRunningTask() {
repeat(10) { i ->
if (!isActive) {
println("Annulé !")
return
}

println("Tâche $i")
delay(500)
}
}

fun main() = runBlocking {
val job = launch {
longRunningTask()
}

delay(2000)
println("Annulation de la tâche !")
job.cancelAndJoin()
}

ensureActive

suspend fun heavyWork() {
repeat(10) { i ->
ensureActive() // Lance une exception si annulé

println("Travail lourd $i")
Thread.sleep(500) // Simulation de travail CPU
}
}

fun main() = runBlocking {
val job = launch {
try {
heavyWork()
} catch (e: CancellationException) {
println("Le travail a été annulé")
}
}

delay(2000)
job.cancel()
}

🤔 Questions fréquentes

Q1. Les fonctions suspend créent-elles des threads ?

R : Non ! suspend ne fait que suspendre sans bloquer le thread.

suspend fun example() {
println("Début : ${Thread.currentThread().name}")
delay(1000)
println("Fin : ${Thread.currentThread().name}")
}
// Peut s'exécuter sur le même thread !

Q2. Et si on rend toutes les fonctions suspend ?

R : Ce n'est pas nécessaire ! Utilisez-le uniquement quand vous avez des opérations asynchrones.

// ❌ Inutile
suspend fun add(a: Int, b: Int) = a + b

// ✅ Nécessaire
suspend fun fetchData() = withContext(Dispatchers.IO) {
// Requête réseau
}

Q3. Peut-on appeler une fonction normale depuis une fonction suspend ?

R : Bien sûr que oui !

fun normalFunction() = println("Fonction normale")

suspend fun suspendFunction() {
normalFunction() // OK !
delay(1000)
}

🎬 Conclusion

Écrivez du code asynchrone efficace avec les fonctions suspend !

Résumé :
✅ Suspension possible avec suspend
✅ Attente sans bloquer le thread
✅ Changement de thread avec withContext
✅ Contrôle de l'exécution séquentielle/parallèle
✅ Tâches annulables

Prochaine étape : Découvrez les flux de données asynchrones avec Flow !