🎛️ Context & Dispatcher
📖 Qu'est-ce que CoroutineContext ?
CoroutineContext est un ensemble de configurations qui détermine comment une coroutine sera exécutée. Il spécifie sur quel thread elle s'exécutera, quel est son nom, etc. !
💡 Dispatcher
Les Dispatchers de base
fun main() = runBlocking {
// Main - Thread UI (Android/Desktop)
launch(Dispatchers.Main) {
// Mise à jour de l'UI
}
// IO - Opérations réseau/fichiers
launch(Dispatchers.IO) {
println("IO: ${Thread.currentThread().name}")
}
// Default - Tâches intensives en CPU
launch(Dispatchers.Default) {
println("Default: ${Thread.currentThread().name}")
}
// Unconfined - Cas spécial
launch(Dispatchers.Unconfined) {
println("Unconfined: ${Thread.currentThread().name}")
}
delay(100)
}
Dispatchers.IO
suspend fun readFile(): String = withContext(Dispatchers.IO) {
// Lecture de fichier, requêtes réseau, etc.
delay(1000)
"Contenu du fichier"
}
suspend fun writeFile(content: String) = withContext(Dispatchers.IO) {
// Écriture de fichier
delay(500)
println("Fichier sauvegardé: $content")
}
fun main() = runBlocking {
val content = readFile()
writeFile(content)
}
Dispatchers.Default
suspend fun heavyComputation(): Int = withContext(Dispatchers.Default) {
// Calcul intensif en CPU
var result = 0
repeat(1_000_000) {
result += it
}
result
}
fun main() = runBlocking {
val result = heavyComputation()
println("Résultat du calcul: $result")
}
🎯 Exemples pratiques
Dispatcher par couche
// Repository - IO
class UserRepository {
suspend fun fetchUser(id: String): User = withContext(Dispatchers.IO) {
delay(1000) // Requête réseau
User(id, "Hong Gildong")
}
}
// UseCase - Default
class ProcessUserUseCase {
suspend fun process(user: User): ProcessedUser = withContext(Dispatchers.Default) {
// Traitement des données
delay(500)
ProcessedUser(user.name.uppercase())
}
}
data class User(val id: String, val name: String)
data class ProcessedUser(val displayName: String)
fun main() = runBlocking {
val repo = UserRepository()
val useCase = ProcessUserUseCase()
val user = repo.fetchUser("123")
val processed = useCase.process(user)
println("Résultat: ${processed.displayName}")
}
Opérations IO parallèles
suspend fun loadAllData(): Triple<String, String, String> = coroutineScope {
val user = async(Dispatchers.IO) {
delay(1000)
"Données utilisateur"
}
val posts = async(Dispatchers.IO) {
delay(1500)
"Données des publications"
}
val comments = async(Dispatchers.IO) {
delay(800)
"Données des commentaires"
}
Triple(user.await(), posts.await(), comments.await())
}
fun main() = runBlocking {
val time = measureTimeMillis {
val (user, posts, comments) = loadAllData()
println("$user, $posts, $comments")
}
println("Temps écoulé: ${time}ms") // ~1500ms (parallèle)
}
🔧 Combinaison de Context
Nommer les coroutines
fun main() = runBlocking {
launch(CoroutineName("Tâche1")) {
println("Nom: ${coroutineContext[CoroutineName]}")
}
launch(Dispatchers.IO + CoroutineName("TâcheIO")) {
println("Thread: ${Thread.currentThread().name}")
println("Nom: ${coroutineContext[CoroutineName]}")
}
delay(100)
}
Ajouter un Job
fun main() = runBlocking {
val job = Job()
launch(job + Dispatchers.Default) {
println("Exécution de la tâche")
delay(1000)
println("Tâche terminée")
}
delay(500)
println("Annulation de la tâche")
job.cancel()
}
🎨 withContext
Changement de thread
suspend fun complexTask() {
println("Début: ${Thread.currentThread().name}")
// Opération IO
val data = withContext(Dispatchers.IO) {
println("IO: ${Thread.currentThread().name}")
"données"
}
// Opération CPU
val processed = withContext(Dispatchers.Default) {
println("Default: ${Thread.currentThread().name}")
data.uppercase()
}
println("Fin: ${Thread.currentThread().name}")
println("Résultat: $processed")
}
fun main() = runBlocking {
complexTask()
}
Modèle d'optimisation
// ❌ Changement inutile
suspend fun bad() {
withContext(Dispatchers.IO) {
val data1 = loadData1()
withContext(Dispatchers.Default) { // Inutile !
process(data1)
}
}
}
// ✅ Efficace
suspend fun good() {
val data1 = withContext(Dispatchers.IO) {
loadData1()
}
withContext(Dispatchers.Default) {
process(data1)
}
}
suspend fun loadData1() = delay(100)
suspend fun process(data: Unit) = delay(100)
🔥 Modèles pratiques
Cache + Réseau
class DataSource {
private var cache: String? = null
suspend fun getData(): String {
// Vérifier le cache (rapide)
cache?.let { return it }
// Requête réseau (lente)
return withContext(Dispatchers.IO) {
delay(1000)
"Nouvelles données"
}.also { cache = it }
}
}
fun main() = runBlocking {
val source = DataSource()
// Premier appel - Réseau
val time1 = measureTimeMillis {
println(source.getData())
}
println("Premier appel: ${time1}ms")
// Deuxième - Cache
val time2 = measureTimeMillis {
println(source.getData())
}
println("Deuxième appel: ${time2}ms")
}
Traitement par lots
suspend fun processBatch(items: List<Int>): List<Int> {
return withContext(Dispatchers.Default) {
items.map { item ->
// Traiter chaque élément
item * 2
}
}
}
fun main() = runBlocking {
val items = List(100) { it }
val results = processBatch(items)
println("Traitement terminé: ${results.size} éléments")
}
Avec timeout
suspend fun fetchWithTimeout(): String? {
return try {
withTimeout(2000) {
withContext(Dispatchers.IO) {
delay(3000) // Prend trop de temps
"données"
}
}
} catch (e: TimeoutCancellationException) {
null
}
}
fun main() = runBlocking {
val result = fetchWithTimeout()
println("Résultat: ${result ?: "Timeout"}")
}
🛡️ Gestion des exceptions
CoroutineExceptionHandler
fun main() = runBlocking {
val handler = CoroutineExceptionHandler { _, exception ->
println("Gestion de l'erreur: ${exception.message}")
}
val job = launch(handler) {
throw Exception("Problème survenu !")
}
job.join()
println("Continue l'exécution")
}
SupervisorJob
fun main() = runBlocking {
val supervisor = SupervisorJob()
with(CoroutineScope(coroutineContext + supervisor)) {
val job1 = launch {
delay(500)
throw Exception("Tâche 1 échouée")
}
val job2 = launch {
delay(1000)
println("Tâche 2 réussie !")
}
try {
job1.join()
} catch (e: Exception) {
println("Exception tâche 1: ${e.message}")
}
job2.join()
}
}
🎯 Dispatcher personnalisé
Spécifier la taille du pool de threads
fun main() = runBlocking {
val customDispatcher = Dispatchers.IO.limitedParallelism(2)
repeat(5) { i ->
launch(customDispatcher) {
println("Tâche $i: ${Thread.currentThread().name}")
delay(1000)
}
}
delay(3000)
}
Thread unique
fun main() = runBlocking {
val singleThread = Dispatchers.Default.limitedParallelism(1)
repeat(3) { i ->
launch(singleThread) {
println("Tâche $i: ${Thread.currentThread().name}")
delay(500)
}
}
delay(2000)
}
🤔 Questions fréquemment posées
Q1. Quel Dispatcher dois-je utiliser ?
R: Choisissez selon le type de tâche !
// IO - Réseau, fichiers, base de données
suspend fun fetchData() = withContext(Dispatchers.IO) { }
// Default - Calculs intensifs en CPU
suspend fun compute() = withContext(Dispatchers.Default) { }
// Main - Mise à jour de l'UI (Android/Desktop)
suspend fun updateUI() = withContext(Dispatchers.Main) { }
Q2. Puis-je utiliser withContext plusieurs fois ?
R: Oui ! Changez de contexte chaque fois que nécessaire.
suspend fun workflow() {
val data = withContext(Dispatchers.IO) {
loadFromNetwork()
}
val processed = withContext(Dispatchers.Default) {
processData(data)
}
withContext(Dispatchers.Main) {
updateUI(processed)
}
}
Q3. Que se passe-t-il si je ne spécifie pas de Dispatcher ?
R: Il hérite du Context de la coroutine parente !
fun main() = runBlocking(Dispatchers.Default) {
launch { // Hérite de Dispatchers.Default
println(Thread.currentThread().name)
}
}
🎬 Conclusion
Contrôlez vos coroutines avec Context et Dispatcher !
Résumé des points clés:
✅ Dispatchers.IO - Réseau/fichiers
✅ Dispatchers.Default - Tâches CPU
✅ Dispatchers.Main - Mise à jour UI
✅ Changement de thread avec withContext
✅ Contrôle fin avec la combinaison de Context
Félicitations ! Vous avez terminé la série sur les Coroutines ! 🎉
Prochaine étape: Apprenez à écrire des tests dans Tests unitaires !