viewModelScope : ce que c'est, liaison avec ViewModel et fonctionnement sous Android

Auteur : IT Sectr Publié le : 2026-06-23 Temps de lecture : 9 min

viewModelScope est un CoroutineScope intégré de la bibliothèque androidx.lifecycle qui est lié au cycle de vie de ViewModel et automatiquement annulé lors de son nettoyage. Selon Google Android Developers, 2025, viewModelScope est le mécanisme standard pour lancer des coroutines dans l'architecture MVVM, garantissant des opérations asynchrones sûres sans risque de fuite mémoire. ViewModelScope utilise Dispatchers.Main par défaut, et toutes les opérations d'IO à l'intérieur doivent être exécutées via withContext.

Points clés

  • viewModelScope — CoroutineScope de lifecycle-viewmodel-ktx, annulé lors de l'appel à ViewModel.onCleared()
  • Dispatchers.Main — dispatcher par défaut, donc les mises à jour de l'UI dans les coroutines sont sûres
  • onCleared — callback qui déclenche l'annulation automatique de toutes les coroutines actives dans viewModelScope
  • clear() vs onCleared() — clear() est appelé par le framework avant onCleared, garantissant l'annulation du scope
  • launch — la principale façon de lancer des coroutines dans viewModelScope pour les opérations fire-and-forget

Qu'est-ce que viewModelScope sous Android ?

viewModelScope est une propriété d'extension sur l'interface ViewModel, ajoutée dans la bibliothèque lifecycle-viewmodel-ktx (à partir de la version 2.1.0). Elle fournit un CoroutineScope prêt à l'emploi lié au cycle de vie de ViewModel.

kotlin
// Internal structure (simplified)
val ViewModel.viewModelScope: CoroutineScope
    get() {
        val scope = this.getTag(JOB_KEY)
        if (scope != null) return scope
        return CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
            .also { setTag(JOB_KEY, it) }
    }

Le scope est créé de manière paresseuse (lazy) au premier accès et mis en cache via setTag. Il utilise SupervisorJob, ce qui signifie qu'une exception dans une coroutine enfant n'annule pas les autres. Le dispatcher par défaut est Dispatchers.Main.immediate, qui exécute le code sur le thread principal sans répartition supplémentaire si déjà sur Main.

Comment viewModelScope reçoit la notification de nettoyage

Lorsque le ViewModel quitte le cycle de vie (l'Activity est terminée ou le Fragment est supprimé), le système appelle clear(), qui déclenche onCleared(). Dans ce callback, viewModelScope annule son Job, ce qui termine récursivement toutes les coroutines actives. Le mécanisme est implémenté via l'interface Closeable, où le Job du scope est enregistré comme ressource pour fermeture automatique.

Comment fonctionne viewModelScope : liaison avec le cycle de vie de ViewModel

Le mécanisme de liaison de viewModelScope au cycle de vie de ViewModel est basé sur le marquage (tagging) et le callback onCleared. Voyons-le étape par étape.

Étape 1 : Création du scope au premier accès

Lorsque le ViewModel exécute viewModelScope.launch { ... }, le getter vérifie si un scope est déjà stocké sous le tag JOB_KEY. Si aucun scope n'existe, une nouvelle instance de CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) est créée. Le scope est stocké dans le ViewModel via une map interne de tags.

Étape 2 : Cycle de vie des coroutines

Toutes les coroutines lancées via viewModelScope.launch ou viewModelScope.async deviennent enfants du SupervisorJob du scope. Elles s'exécutent sur le thread principal (sauf si un autre dispatcher est spécifié via withContext). Tant que le ViewModel est vivant, les coroutines peuvent être actives, suspendues ou terminées.

Étape 3 : Annulation lors de onCleared

Lorsque le système détruit le ViewModel, ViewModel.clear() est appelé. À l'intérieur de clear(), ce qui suit se produit :

  • onCleared() est appelé pour la logique de nettoyage personnalisée
  • Toutes les ressources Closeable enregistrées via addCloseable sont fermées
  • Le Job de viewModelScope passe à l'état Cancelled
  • Toutes les coroutines enfants sont annulées récursivement
  • Les références au scope sont libérées pour le ramasse-miettes

Résistance à la rotation

Lorsque l'écran est tourné, l'Activity est recréée, mais le ViewModel survit (grâce à ViewModelStoreOwner). Cela signifie que viewModelScope reste actif et que les coroutines continuent de s'exécuter sans interruption. Après la recréation de l'Activity, le même ViewModel (et le même scope) est réutilisé — le chargement des données ne repart pas de zéro.

viewModelScope dans l'architecture MVVM

MVVM (Model-View-ViewModel) est l'architecture recommandée par Google pour les applications Android. viewModelScope y occupe une place centrale en tant qu'exécuteur d'opérations asynchrones.

Le rôle de viewModelScope dans les couches de l'architecture

CoucheComposantRôle de viewModelScope
UIActivity / FragmentObserve StateFlow/LiveData du ViewModel
ViewModelViewModelLance des coroutines via viewModelScope, gère l'état de l'UI
RepositoryRepositoryExpose des fonctions suspend appelées depuis les coroutines de viewModelScope
DataDAO / ApiExécute les requêtes réelles (Room, Retrofit)

Le ViewModel lance des coroutines via viewModelScope, à l'intérieur desquelles il appelle des fonctions suspend du Repository. Le résultat est transformé en StateFlow, qui est observé par la couche UI. Cette conception garantit une séparation claire des responsabilités et la testabilité indépendante de chaque couche.

Pourquoi viewModelScope dans ViewModel et pas dans Fragment

Si les coroutines étaient lancées depuis un Fragment, elles seraient annulées lors de la rotation de l'écran avec la destruction du Fragment. Le ViewModel survit à la rotation, donc les coroutines lancées dans son scope continuent de s'exécuter. C'est l'avantage clé de viewModelScope par rapport à lifecycleScope lors du chargement de données.

Exemples d'utilisation de viewModelScope

Examinons trois scénarios pratiques d'utilisation de viewModelScope dans une application Android avec Kotlin.

Exemple 1 : Chargement de données à la création du ViewModel

kotlin
class ProfileViewModel(
    private val repo: ProfileRepository
) : ViewModel() {

    private val _profile = MutableStateFlow<Profile?>(null)
    val profile: StateFlow<Profile?> = _profile

    init {
        loadProfile()
    }

    private fun loadProfile() {
        viewModelScope.launch {
            val result = repo.getProfile()
            _profile.value = result
        }
    }
}

Dans le bloc init, le chargement du profil commence immédiatement. La coroutine s'exécute sur le thread principal (par défaut). Le référentiel utilise withContext(Dispatchers.IO) pour la requête réseau à l'intérieur de sa fonction suspend, donc le ViewModel n'a pas à se soucier du changement de threads.

Exemple 2 : Gestion des erreurs avec sealed class

kotlin
sealed class UiState {
    object Loading : UiState()
    data class Success(val data: List<Item>) : UiState()
    data class Error(val message: String) : UiState()
}
kotlin
fun fetchItems() {
    _state.value = UiState.Loading
    viewModelScope.launch {
        try {
            val items = repo.getItems()
            _state.value = UiState.Success(items)
        } catch (e: Exception) {
            _state.value = UiState.Error(e.message ?: "Unknown error")
        }
    }
}

L'état de l'UI est décrit via une sealed class UiState. Le ViewModel met à jour l'état à chaque changement. Le Fragment s'abonne au StateFlow et réagit uniquement à l'état actuel, ignorant les appels obsolètes des rotations précédentes.

Exemple 3 : Annulation de la coroutine précédente lors d'une nouvelle requête

kotlin
private var searchJob: Job? = null

fun search(query: String) {
    searchJob?.cancel()
    searchJob = viewModelScope.launch {
        delay(300)
        val results = repo.search(query)
        _searchResults.value = results
    }
}

À chaque nouvelle requête de recherche, la coroutine précédente est annulée. delay(300) implémente le debounce — la recherche n'est exécutée qu'après 300 ms d'inactivité. Cela réduit la charge du serveur et évite les résultats obsolètes.

viewModelScope vs lifecycleScope : quand choisir lequel

Les deux scopes sont fournis par la bibliothèque AndroidX Lifecycle, mais sont liés à des cycles de vie différents. Le choix dépend du type de tâche.

Comparaison des scopes

CaractéristiqueviewModelScopelifecycleScope
PropriétaireViewModelLifecycleOwner (Activity/Fragment)
Annulé à la rotationNon (ViewModel survit)Oui (Activity est recréée)
Dispatcher par défautDispatchers.Main.immediateDispatchers.Main.immediate
Disponible dansViewModelActivity, Fragment, Service
Cas d'usage typiqueChargement de données, logique métierInteractions UI, animations

Recommandations Google

Google recommande d'utiliser viewModelScope pour toutes les tâches de chargement et de traitement de données. lifecycleScope doit être utilisé pour les opérations liées à un moment spécifique du cycle de vie de l'UI — par exemple, lancer une animation à la première apparition de l'écran ou s'abonner aux mises à jour de localisation qui doivent cesser en quittant l'écran.

Erreurs courantes lors de l'utilisation de viewModelScope

Même dans une API Android bien documentée, les développeurs commettent des erreurs typiques. Examinons quatre des problèmes les plus courants.

Erreur 1 : Mettre à jour l'UI après l'annulation du scope

L'erreur la plus insidieuse est de tenter de mettre à jour StateFlow ou LiveData après que le ViewModel a été nettoyé. Bien que viewModelScope soit annulé lors de onCleared(), une coroutine peut exécuter du code avant que l'annulation prenne effet. Utilisez isActive pour vérifier ou fiez-vous à la fin du bloc catch.

Erreur 2 : Lancer des coroutines sans considérer SupervisorJob

viewModelScope utilise SupervisorJob en interne, ce qui isole les erreurs entre les coroutines. Cependant, si vous lancez une coroutine avec son propre Job() dans viewModelScope.launch, cette coroutine devient un enfant de SupervisorJob mais ne sera pas protégée contre l'annulation causée par des erreurs dans d'autres coroutines.

Erreur 3 : Trop de coroutines dans un seul scope

Bien que viewModelScope n'ait pas de limite stricte, des milliers de coroutines actives peuvent ralentir le système. Pour les longues listes de données, utilisez Flow avec collectLatest au lieu de créer des coroutines séparées pour chaque élément.

Erreur 4 : Utiliser GlobalScope au lieu de viewModelScope

Si GlobalScope est importé accidentellement au lieu de viewModelScope, la coroutine ne sera pas annulée lors du nettoyage du ViewModel. Cela entraîne des fuites de mémoire et des plantages potentiels. Assurez-vous toujours que les coroutines sont lancées via viewModelScope, en particulier dans les sous-classes de Fragment.

Foire aux questions

Puis-je changer le dispatcher par défaut de viewModelScope ?

Vous ne pouvez pas changer directement le dispatcher de viewModelScope — il est codé en dur comme Dispatchers.Main.immediate. Cependant, à l'intérieur d'une coroutine, vous pouvez passer à un autre dispatcher via withContext. Pour changer le dispatcher dans les tests, utilisez TestDispatcher via une Rule.

Comment passer viewModelScope au Repository ?

Ne passez pas le scope au Repository — cela viole les principes architecturaux. Le Repository doit exposer des fonctions suspend, et le ViewModel lui-même gère les coroutines via viewModelScope. Si le Repository nécessite un scope, repensez l'architecture en faveur de Clean Architecture.

Pourquoi viewModelScope utilise-t-il SupervisorJob ?

SupervisorJob garantit qu'une exception dans une coroutine (par exemple, une erreur de chargement dans l'une de plusieurs requêtes indépendantes) n'annule pas les autres coroutines. Cela correspond au scénario ViewModel, où différents écrans chargent des données indépendantes.

viewModelScope est-il disponible dans Jetpack Compose ?

Oui, viewModelScope est disponible dans tout ViewModel quel que soit le type d'UI (View System ou Jetpack Compose). Dans Compose, les coroutines sont également lancées via viewModelScope, tandis que les effets d'UI utilisent LaunchedEffect et rememberCoroutineScope.

Que devient une coroutine lorsque viewModelScope.cancel() est appelé ?

L'appel de viewModelScope.cancel() annule le scope immédiatement — toutes les coroutines actives se terminent par CancellationException. Si viewModelScope.launch est appelé ensuite, un nouveau scope est créé automatiquement lors du prochain accès au getter.

Résumé

  • viewModelScope — CoroutineScope lié au cycle de vie de ViewModel, automatiquement annulé dans onCleared()
  • SupervisorJob + Dispatchers.Main — configuration interne garantissant l'isolation des erreurs et un accès sûr à l'UI
  • Rotation d'écran — le ViewModel survit, donc les coroutines dans viewModelScope continuent sans redémarrage
  • Architecture MVVM — viewModelScope est l'élément central pour les opérations asynchrones dans la couche ViewModel
  • lifecycleScope — alternative pour les opérations liées au cycle de vie d'Activity/Fragment, pas au ViewModel
  • StateFlow — moyen privilégié de transmettre des données des coroutines de viewModelScope à l'UI via sealed class
  • GlobalScope est dangereux — remplacer viewModelScope par GlobalScope entraîne des fuites mémoire et des plantages de l'application

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.

Discuter du projet

Lisez aussi