CoroutineScope est une interface Kotlin qui définit le cycle de vie d'une coroutine et fournit un contexte pour lancer de nouvelles coroutines. Selon la documentation Kotlin, 2025, chaque instance CoroutineScope contient un CoroutineContext et gère toutes les coroutines lancées à l'intérieur. Lorsque le scope est annulé (cancel), toutes les coroutines filles sont automatiquement annulées, ce qui évite les fuites mémoire.
Points clés
CoroutineScope est une interface fondamentale de la bibliothèque kotlinx.coroutines qui sert de conteneur pour les coroutines. Elle définit les limites du cycle de vie des coroutines : lorsque le scope se termine, toutes les coroutines à l'intérieur sont automatiquement annulées.
public interface CoroutineScope {
public val coroutineContext: CoroutineContext
}
L'interface ne contient qu'un seul champ — coroutineContext. À travers lui, le scope fournit un dispatcher (Dispatcher), un job (Job), un gestionnaire d'exceptions et d'autres éléments de contexte à toutes les coroutines lancées à l'intérieur.
Toutes les fonctions de lancement de coroutines — launch, async, runBlocking — sont des fonctions d'extension sur CoroutineScope. Cela signifie qu'elles ne peuvent être appelées que lorsqu'un objet scope est disponible. Cette conception garantit que chaque coroutine a un parent et un cycle de vie clairement définis.
Dans Android, chaque composant architectural a son propre scope : viewModelScope pour ViewModel, lifecycleScope pour Activity/Fragment. Dans les applications serveur, un scope peut être lié à une requête HTTP ou à un pool de connexions de base de données.
Comprendre le fonctionnement interne de CoroutineScope nécessite une familiarité avec le concept de Job et le principe de concurrence structurée.
Chaque coroutine au lancement retourne un objet Job (ou Deferred pour async). Job représente une tâche avec un cycle de vie fini : New, Active, Completing, Completed, Cancelling, Cancelled. Les objets Job forment une structure arborescente :
Concurrence structurée est un principe architectural clé de Kotlin Coroutines, où le cycle de vie de la coroutine est lié au cycle de vie de son scope. Cela contraste avec le modèle « tirer et oublier », où une coroutine continue de vivre après la fin du scope. Avantages de la concurrence structurée :
Lorsque scope.cancel() est appelé, le Job du scope passe à l'état Cancelled, ce qui annule récursivement tous les Jobs fils. Après l'annulation, le scope ne peut être réutilisé qu'en créant une nouvelle instance de CoroutineScope.
Vous pouvez créer un CoroutineScope via une fonction d'usine ou en implémentant l'interface dans votre classe. Examinons les deux approches.
val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
scope.launch {
println("Running on ${Thread.currentThread().name}")
}
La fonction d'usine prend un CoroutineContext et crée un scope avec le contexte spécifié. L'exemple utilise Dispatchers.Default pour les tâches intensives en CPU et SupervisorJob, qui isole les exceptions entre coroutines filles.
class MyRepository {
private val scope = CoroutineScope(Dispatchers.IO + Job())
suspend fun fetchData(): Data = scope.async {
api.getData()
}.await()
fun cleanup() {
scope.cancel()
}
}
Nous stockons le scope comme un champ de classe et appelons manuellement cleanup pour l'annuler. Cette approche convient aux composants avec un cycle de vie géré — par exemple, les repositories ou les gestionnaires.
Kotlin permet de déléguer l'implémentation de CoroutineScope via le mot-clé by :
class DataLoader : CoroutineScope by CoroutineScope(Dispatchers.IO) {
fun load() {
launch {
// coroutine runs in DataLoader scope
}
}
}
Cette approche est pratique lorsque la classe elle-même est un scope et souhaite fournir des méthodes de lancement de coroutines. Cependant, attention : la classe hérite de toutes les méthodes de CoroutineScope, y compris cancel, ce qui peut briser l'encapsulation.
GlobalScope est un singleton CoroutineScope pour toute l'application. Son utilisation en code de production n'est officiellement pas recommandée.
JetBrains autorise GlobalScope uniquement dans des scénarios rares : processus d'arrière-plan au niveau de l'application qui doivent vivre même après la fermeture de toutes les Activities (par exemple, synchronisation de données, analytique). Mais même dans ces cas, il est préférable de créer votre propre scope avec CoroutineScope(SupervisorJob()).
Utilisez toujours un CoroutineScope personnalisé avec une gestion explicite du cycle de vie. Dans Android, ce sont viewModelScope et lifecycleScope. Dans les applications serveur, créez un scope pour chaque requête ou pool de connexions.
Les deux fonctions sont des fonctions suspend qui créent un scope temporaire pour les tâches parallèles, mais leur comportement face aux exceptions diffère fondamentalement.
| Caractéristique | coroutineScope | supervisorScope |
|---|---|---|
| Comportement en cas d'erreur | Une exception dans une coroutine fille annule toutes les autres | Une exception dans une coroutine fille n'annule PAS les autres |
| Propagation d'erreur | Oui, la première exception est propagée vers l'extérieur | Oui, la première exception est propagée vers l'extérieur |
| Job par défaut | Job() — les fils sont liés au parent | SupervisorJob() — les fils ne dépendent pas les uns des autres |
| Cas d'utilisation typique | Opération atomique en plusieurs étapes | Tâches parallèles indépendantes (chargements UI) |
Utilisez coroutineScope lorsque plusieurs opérations parallèles forment une seule opération atomique. Par exemple, charger des données depuis trois serveurs : si une requête échoue, les autres n'ont pas de sens.
suspend fun loadProductPage(): ProductPage = coroutineScope {
val product = async { api.getProduct() }
val reviews = async { api.getReviews() }
ProductPage(product.await(), reviews.await())
}
Si getProduct ou getReviews lève une exception — les deux coroutines sont annulées et l'exception est propagée au code appelant.
Utilisez supervisorScope lorsque les opérations parallèles ne dépendent pas les unes des autres. Par exemple, charger les données de profil dans plusieurs sections indépendantes : si la section des recommandations échoue, l'en-tête du profil et la liste d'amis doivent s'afficher.
Examinons les erreurs les plus fréquentes des développeurs lors de l'utilisation de CoroutineScope dans Kotlin.
Le scénario le plus courant de fuite de coroutine est la création d'un scope sans appeler cancel à la fin du composant. Si le scope n'est pas annulé, les coroutines continuent de s'exécuter, maintenant des références aux objets. Dans Android, utilisez viewModelScope ou lifecycleScope, qui sont annulés automatiquement.
GlobalScope ignore le cycle de vie des composants Android. Une coroutine lancée dans GlobalScope après la fermeture d'une Activity continuera à s'exécuter et tentera de mettre à jour l'UI — ce qui provoquera un crash. Utilisez toujours lifecycleScope pour les composants UI.
Après avoir appelé cancel(), le scope ne peut pas être réutilisé — toutes les coroutines à l'intérieur sont déjà terminées. Créez une nouvelle instance de CoroutineScope via la fonction d'usine. Job() ne prend pas en charge la réactivation.
En déléguant avec by, la classe obtient une méthode cancel() publique qui peut être appelée de n'importe où, brisant l'encapsulation. Stockez le scope comme un champ privé plutôt que de déléguer l'interface.
Questions fréquentes
CoroutineScope est une interface qui possède un CoroutineContext et est responsable du cycle de vie des coroutines. CoroutineContext est un ensemble d'éléments (dispatcher, job, gestionnaire d'erreurs) qui définit « comment » une coroutine s'exécute. Une différence : le scope crée des coroutines, tandis que le contexte contrôle leur comportement.
Oui, c'est un modèle standard : CoroutineScope(Dispatchers.IO + SupervisorJob()). SupervisorJob empêche l'annulation en cascade des coroutines filles lorsque l'une d'elles lève une exception. C'est utile pour les tâches parallèles indépendantes où une erreur dans l'une ne doit pas arrêter les autres.
Il n'y a pas de limite sur le nombre de coroutines dans un scope — elles sont seulement limitées par la mémoire disponible et les paramètres du dispatcher. La limite pratique est typiquement de milliers de coroutines actives dans un seul scope. Cependant, un grand nombre de coroutines peut indiquer des problèmes architecturaux.
La bonne méthode est de passer le scope à la classe via le constructeur ou d'utiliser runBlockingTest / runTest de kotlinx-coroutines-test. Dans les tests, vous pouvez remplacer le scope par TestCoroutineDispatcher et contrôler l'exécution des coroutines manuellement.
Non, un scope est un conteneur externe pour une coroutine. La coroutine elle-même n'est pas un scope. Cependant, à l'intérieur d'une coroutine, vous pouvez créer un nouveau scope via coroutineScope ou supervisorScope pour lancer des coroutines filles en parallèle.
Résumé
Nous développerons une application mobile clé en main
IT Sectr crée des applications iOS et Android pour les startups et les entreprises depuis 2017. Nous vous conseillerons et vous proposerons la meilleure solution.
Lisez aussi