viewModelScope: wat is het, koppeling aan ViewModel en werking in Android

Auteur: IT Sectr Gepubliceerd: 2026-06-23 Leestijd: 9 min

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 — CoroutineScope uit lifecycle-viewmodel-ktx, geannuleerd bij onCleared() van ViewModel
  • Dispatchers.Main — standaard dispatcher, dus UI-updates binnen coroutines zijn veilig
  • onCleared — callback waarbij viewModelScope automatisch alle actieve coroutines annuleert
  • clear() vs onCleared() — clear() wordt door het framework aangeroepen vóór onCleared, wat annulering van de scope garandeert
  • launch — de primaire manier om coroutines te starten in viewModelScope voor fire-and-forget bewerkingen

Wat is viewModelScope in Android?

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.

kotlin
// 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.

Hoe ontvangt viewModelScope de opschoningsmelding?

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.

Hoe werkt viewModelScope: koppeling aan de levenscyclus van ViewModel

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.

Stap 1: Aanmaken van de scope bij eerste toegang

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.

Stap 2: Levenscyclus van coroutines

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.

Stap 3: Annulering bij onCleared

Wanneer het systeem ViewModel vernietigt, wordt ViewModel.clear() aangeroepen. Binnen clear() gebeurt het volgende:

  • Aanroep van onCleared() voor gebruikerslogica
  • Sluiten van alle Closeable-resources geregistreerd via addCloseable
  • Job van viewModelScope gaat naar de status Cancelled
  • Alle onderliggende coroutines worden recursief geannuleerd
  • Vrijgeven van verwijzingen naar de scope voor de garbage collector

Weerstand tegen schermrotatie

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.

viewModelScope in MVVM-architectuur

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.

Rol van viewModelScope in de architectuurlagen

LaagComponentRol van viewModelScope
UIActivity / FragmentObserveert StateFlow/LiveData uit ViewModel
ViewModelViewModelStart coroutines via viewModelScope, beheert UI-status
RepositoryRepositoryOntvangt suspend-functies aangeroepen vanuit viewModelScope coroutines
DataDAO / ApiVoert 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.

Waarom viewModelScope in ViewModel, niet in Fragment

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.

Voorbeelden van het gebruik van viewModelScope

Laten we drie praktische scenario's bekijken van het gebruik van viewModelScope in een Android-applicatie in Kotlin.

Voorbeeld 1: Gegevens laden bij het aanmaken van 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
        }
    }
}

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.

Voorbeeld 2: Foutafhandeling via 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")
        }
    }
}

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.

Voorbeeld 3: Annuleren van de vorige coroutine bij een nieuw verzoek

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
    }
}

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.

viewModelScope vs lifecycleScope: wanneer wat te kiezen

Beide scopes worden geleverd door de AndroidX Lifecycle-bibliotheek, maar zijn gekoppeld aan verschillende levenscycli. De keuze hangt af van het type taak.

Vergelijking van scopes

KenmerkviewModelScopelifecycleScope
EigenaarViewModelLifecycleOwner (Activity/Fragment)
Annulering bij rotatieNee (ViewModel blijft behouden)Ja (Activity wordt opnieuw aangemaakt)
Standaard dispatcherDispatchers.Main.immediateDispatchers.Main.immediate
Beschikbaar inViewModelActivity, Fragment, Service
Typisch scenarioGegevens laden, bedrijfslogicaUI-interactie, animaties, snackbars

Aanbevelingen van Google

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.

Veelvoorkomende fouten bij het werken met viewModelScope

Zelfs in de goed gedocumenteerde API van Android maken ontwikkelaars karakteristieke fouten. Laten we vier veelvoorkomende problemen bekijken.

Fout 1: UI updaten na annulering van de scope

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.

Fout 2: Coroutines starten zonder rekening te houden met SupervisorJob

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.

Fout 3: Te veel coroutines in één scope

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.

Fout 4: Gebruik van GlobalScope in plaats van viewModelScope

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

Kan ik de standaard dispatcher van viewModelScope wijzigen?

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.

Hoe geef ik viewModelScope door aan Repository?

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.

Waarom gebruikt viewModelScope SupervisorJob?

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.

Is viewModelScope beschikbaar in Jetpack Compose?

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.

Wat gebeurt er met een coroutine bij aanroep van viewModelScope.cancel()?

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

  • viewModelScope — CoroutineScope gekoppeld aan de levenscyclus van ViewModel, automatisch geannuleerd bij onCleared()
  • SupervisorJob + Dispatchers.Main — interne configuratie die foutisolatie en veilige toegang tot UI garandeert
  • Schermrotatie — ViewModel blijft behouden, dus coroutines in viewModelScope gaan door zonder herstart
  • MVVM-architectuur — viewModelScope is het centrale element voor asynchrone bewerkingen in de ViewModel-laag
  • lifecycleScope — alternatief voor bewerkingen gekoppeld aan de levenscyclus van Activity/Fragment, niet ViewModel
  • StateFlow — de voorkeursmethode voor gegevensoverdracht van viewModelScope-coroutines naar UI via sealed class
  • GlobalScope is gevaarlijk — vervanging van viewModelScope door GlobalScope leidt tot geheugenlekken en app-crashes

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.

Bespreek het project

Lees ook