viewModelScope — is een ingebouwde CoroutineScope uit de androidx.lifecycle-bibliotheek, die is gekoppeld aan de levenscyclus van ViewModel en automatisch wordt geannuleerd bij het opschonen ervan. Volgens Google Android Developers, 2025 is viewModelScope het standaardmechanisme voor het starten van coroutines in MVVM-architectuur, wat veilig werken met asynchrone bewerkingen mogelijk maakt zonder risico op geheugenlekken. ViewModelScope gebruikt standaard Dispatchers.Main en alle IO-bewerkingen erin moeten via withContext worden uitgevoerd.
Belangrijkste punten
viewModelScope — is een extensie-eigenschap (extension property) op de ViewModel-interface, toegevoegd in de lifecycle-viewmodel-ktx-bibliotheek (sinds versie 2.1.0). Het biedt een kant-en-klare CoroutineScope die is gekoppeld aan de levenscyclus van ViewModel.
// Interne structuur (vereenvoudigd)
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) }
}
De scope wordt lui (lazy) aangemaakt bij de eerste toegang en gecachet via setTag. Er wordt SupervisorJob gebruikt, wat betekent dat een uitzondering in één onderliggende coroutine de andere niet annuleert. De standaard dispatcher is Dispatchers.Main.immediate, die code op de hoofdthread uitvoert zonder extra dispatch als de aanroep al op Main is.
Wanneer ViewModel de levenscyclus verlaat (Activity is voltooid of Fragment is verwijderd), roept het systeem clear() aan, wat onCleared() activeert. In deze callback annuleert viewModelScope zijn Job, wat recursief alle actieve coroutines beëindigt. Het mechanisme is geïmplementeerd via de Closeable-interface, waarbij de Job van de scope wordt geregistreerd als een resource voor automatische sluiting.
Het mechanisme van de koppeling van viewModelScope aan de levenscyclus van ViewModel is gebaseerd op tagging en de callback onCleared. Laten we stap voor stap bekijken hoe dit werkt.
Wanneer ViewModel viewModelScope.launch { ... } uitvoert, controleert de getter of er een opgeslagen scope bestaat onder de tag JOB_KEY. Als de scope nog niet is aangemaakt — wordt een nieuwe instantie CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) gemaakt. De scope wordt opgeslagen in de ViewModel via een interne tag-kaart.
Alle coroutines die zijn gestart via viewModelScope.launch of viewModelScope.async worden onderliggend aan de SupervisorJob van de scope. Ze werken op de hoofdthread (tenzij een andere dispatcher is opgegeven via withContext). Zolang ViewModel leeft — kunnen coroutines actief, geschorst of voltooid zijn.
Wanneer het systeem ViewModel vernietigt, wordt ViewModel.clear() aangeroepen. Binnen clear() gebeurt het volgende:
Bij schermrotatie wordt Activity opnieuw aangemaakt, maar ViewModel blijft behouden (dankzij ViewModelStoreOwner). Dit betekent dat viewModelScope actief blijft en coroutines ononderbroken verder kunnen werken. Na het opnieuw aanmaken van Activity wordt dezelfde ViewModel (en dezelfde scope) opnieuw gebruikt — het laden van gegevens begint niet opnieuw.
MVVM (Model-View-ViewModel) — is de door Google aanbevolen architectuur voor Android-applicaties. viewModelScope neemt er een centrale plaats in als uitvoerder van asynchrone bewerkingen.
| Laag | Component | Rol van viewModelScope |
|---|---|---|
| UI | Activity / Fragment | Observeert StateFlow/LiveData uit ViewModel |
| ViewModel | ViewModel | Start coroutines via viewModelScope, beheert UI-status |
| Repository | Repository | Ontvangt suspend-functies aangeroepen vanuit viewModelScope coroutines |
| Data | DAO / Api | Voert daadwerkelijke verzoeken uit (Room, Retrofit) |
ViewModel start via viewModelScope coroutines, waarbinnen het suspend-functies van Repository aanroept. Het resultaat wordt omgezet in StateFlow, dat wordt geobserveerd door de UI-laag. Dit schema zorgt voor een duidelijke scheiding van verantwoordelijkheden en testbaarheid van elke laag onafhankelijk.
Als coroutines vanuit Fragment zouden worden gestart, zouden ze bij schermrotatie worden geannuleerd samen met de vernietiging van Fragment. ViewModel overleeft rotatie, dus coroutines die in zijn scope zijn gestart, blijven doorlopen. Dit is het belangrijkste voordeel van viewModelScope ten opzichte van lifecycleScope bij het laden van gegevens.
Laten we drie praktische scenario's bekijken van het gebruik van viewModelScope in een Android-applicatie in 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
}
}
}
In het init-blok wordt onmiddellijk het laden van het profiel gestart. De coroutine wordt standaard op de hoofdthread uitgevoerd. De repository gebruikt withContext(Dispatchers.IO) voor het netwerkverzoek binnen zijn suspend-functie, dus ViewModel hoeft zich geen zorgen te maken over threadwisselingen.
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")
}
}
}
De UI-status wordt beschreven via sealed class UiState. ViewModel werkt de state bij bij elke wijziging. Fragment is geabonneerd op StateFlow en reageert alleen op de huidige status, waarbij verouderde aanroepen bij herhaalde rotaties worden genegeerd.
private var searchJob: Job? = null
fun search(query: String) {
searchJob?.cancel()
searchJob = viewModelScope.launch {
delay(300)
val results = repo.search(query)
_searchResults.value = results
}
}
Bij elk nieuw zoekverzoek wordt de vorige coroutine geannuleerd. delay(300) implementeert debounce — de zoekopdracht wordt pas uitgevoerd na 300 ms pauze in de invoer. Dit vermindert de serverbelasting en voorkomt verouderde resultaten.
Beide scopes worden geleverd door de AndroidX Lifecycle-bibliotheek, maar zijn gekoppeld aan verschillende levenscycli. De keuze hangt af van het type taak.
| Kenmerk | viewModelScope | lifecycleScope |
|---|---|---|
| Eigenaar | ViewModel | LifecycleOwner (Activity/Fragment) |
| Annulering bij rotatie | Nee (ViewModel blijft behouden) | Ja (Activity wordt opnieuw aangemaakt) |
| Standaard dispatcher | Dispatchers.Main.immediate | Dispatchers.Main.immediate |
| Beschikbaar in | ViewModel | Activity, Fragment, Service |
| Typisch scenario | Gegevens laden, bedrijfslogica | UI-interactie, animaties, snackbars |
Google raadt aan om viewModelScope te gebruiken voor alle taken die verband houden met het laden en verwerken van gegevens. lifecycleScope moet worden toegepast voor bewerkingen die zijn gekoppeld aan een specifiek moment in het UI-leven — bijvoorbeeld het starten van een animatie bij de eerste weergave van het scherm of het abonneren op Location-updates die moeten stoppen bij het verlaten van het scherm.
Zelfs in de goed gedocumenteerde API van Android maken ontwikkelaars karakteristieke fouten. Laten we vier veelvoorkomende problemen bekijken.
De meest verraderlijke fout — proberen StateFlow of LiveData bij te werken nadat ViewModel is opgeschoond. Hoewel viewModelScope wordt geannuleerd bij onCleared(), kan de coroutine code uitvoeren tot het moment van daadwerkelijke annulering. Gebruik isActive voor controle of vertrouw op de voltooiing van het catch-blok.
viewModelScope gebruikt intern SupervisorJob, wat fouten tussen coroutines isoleert. Maar als u een coroutine start met een eigen Job() binnen viewModelScope.launch, wordt die coroutine onderliggend aan SupervisorJob, maar wordt niet beschermd tegen annulering bij fouten in andere coroutines.
Hoewel viewModelScope geen harde limiet heeft, kunnen duizenden actieve coroutines het systeem vertragen. Gebruik voor lange gegevenslijsten Flow met collectLatest in plaats van aparte coroutines voor elk element te maken.
Als u per ongeluk GlobalScope importeert in plaats van viewModelScope, wordt de coroutine niet geannuleerd bij het opschonen van ViewModel. Dit leidt tot geheugenlekken en mogelijke crashes. Controleer altijd dat coroutines worden gestart via viewModelScope, vooral in overgeërfde fragments.
Veelgestelde vragen
De dispatcher van viewModelScope kan niet direct worden gewijzigd — deze is vast ingesteld als Dispatchers.Main.immediate. Maar binnen een coroutine kan via withContext naar een andere dispatcher worden overgeschakeld. Voor het wijzigen van de dispatcher in tests gebruikt u TestDispatcher via Rule.
Geef de scope niet door aan Repository — dit schendt de architectuurprincipes. Repository moet suspend-functies leveren en ViewModel beheert zelf de coroutines via viewModelScope. Als Repository een scope vereist — heroverweeg de architectuur ten gunste van Clean Architecture.
SupervisorJob garandeert dat een uitzondering in één coroutine (bijvoorbeeld een laadfout van een van meerdere onafhankelijke verzoeken) de andere coroutines niet annuleert. Dit komt overeen met het ViewModel-scenario, waar verschillende schermen onafhankelijke gegevens laden.
Ja, viewModelScope is beschikbaar in elke ViewModel, ongeacht het UI-type (View System of Jetpack Compose). In Compose worden coroutines ook gestart via viewModelScope en voor UI-effecten worden LaunchedEffect en rememberCoroutineScope gebruikt.
Aanroep van viewModelScope.cancel() annuleert de scope onmiddellijk — alle actieve coroutines eindigen met CancellationException. Als u daarna viewModelScope.launch aanroept, wordt een nieuwe scope automatisch aangemaakt bij de volgende toegang tot de getter.
Samenvatting
We ontwikkelen een mobiele applicatie turnkey
IT Sectr creëert sinds 2017 iOS- en Android-applicaties voor startups en bedrijven. We adviseren u en stellen de beste oplossing voor.
Lees ook