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 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.
// 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.
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.
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.
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.
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.
Lorsque le système détruit le ViewModel, ViewModel.clear() est appelé. À l'intérieur de clear(), ce qui suit se produit :
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.
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.
| Couche | Composant | Rôle de viewModelScope |
|---|---|---|
| UI | Activity / Fragment | Observe StateFlow/LiveData du ViewModel |
| ViewModel | ViewModel | Lance des coroutines via viewModelScope, gère l'état de l'UI |
| Repository | Repository | Expose des fonctions suspend appelées depuis les coroutines de viewModelScope |
| Data | DAO / Api | Exé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.
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.
Examinons trois scénarios pratiques d'utilisation de viewModelScope dans une application Android avec 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.
sealed class UiState {
object Loading : UiState()
data class Success(val data: List<Item>) : UiState()
data class Error(val message: String) : UiState()
}
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.
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.
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.
| Caractéristique | viewModelScope | lifecycleScope |
|---|---|---|
| Propriétaire | ViewModel | LifecycleOwner (Activity/Fragment) |
| Annulé à la rotation | Non (ViewModel survit) | Oui (Activity est recréée) |
| Dispatcher par défaut | Dispatchers.Main.immediate | Dispatchers.Main.immediate |
| Disponible dans | ViewModel | Activity, Fragment, Service |
| Cas d'usage typique | Chargement de données, logique métier | Interactions UI, animations |
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.
Même dans une API Android bien documentée, les développeurs commettent des erreurs typiques. Examinons quatre des problèmes les plus courants.
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.
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.
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.
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
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.
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.
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.
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.
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é
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