MVVM: wat is het, het Model-View-ViewModel patroon in mobiele ontwikkeling

Auteur: IT Sectr Gepubliceerd: 2026-02-16 Leestijd: 9 min

MVVM (Model-View-ViewModel) — een architectuurpatroon waarin ViewModel Presenter vervangt en reactieve mechanismen gebruikt voor communicatie met View: ObservableObject in SwiftUI, LiveData/StateFlow in Android. ViewModel heeft geen verwijzing naar View — gegevens worden via abonnement verzonden, wat de noodzaak voor ViewContract-interfaces elimineert en testen nog eenvoudiger maakt. Apple beveelt MVVM met SwiftUI aan sinds 2019, Google — MVVM met Jetpack als de officiële Android-architectuur. Meer in Android Architecture Guide.

Belangrijkste

  • MVVM — Model (gegevens), View (interface), ViewModel (status en logica zonder verwijzing naar View)
  • Reactive binding — LiveData, StateFlow, ObservableObject werken UI automatisch bij bij gegevenswijziging
  • ViewModel — overleeft schermrotatie en is niet afhankelijk van Android SDK/UIKit, getest met unittesten
  • Android Jetpack — ViewModel, LiveData, DataBinding — officiële stack van Google voor MVVM
  • SwiftUI + Combine — native implementatie van MVVM in iOS met @Published en @ObservedObject

Wat is MVVM: de essentie van het Model-View-ViewModel patroon

MVVM (Model-View-ViewModel) — een architectuurpatroon beschreven door John Gossman in 2005 voor Windows Presentation Foundation (WPF) van Microsoft. ViewModel — de centrale component die de schermstatus en bedrijfslogica bevat, maar geen verwijzing naar View heeft. Gegevens worden verzonden via reactieve bindingsmechanismen: View abonneert zich op wijzigingen in ViewModel en wordt automatisch opnieuw getekend wanneer gegevens veranderen.

Het belangrijkste verschil tussen MVVM en MVP — afwezigheid van ViewContract. In MVP roept Presenter view.showUser(data) methoden aan, dat wil zeggen Presenter “duwt” actief gegevens naar View. In MVVM “trekt” View zelf gegevens uit ViewModel via abonnement: ViewModel weet niet of het een abonnee heeft. Dit elimineert het probleem van losgekoppelde View — als Activity wordt vernietigd bij rotatie, blijft ViewModel werken, en nieuwe Activity abonneert zich gewoon op actuele gegevens. Bij IT Sectr gebruiken we MVVM in alle nieuwe projecten sinds 2020 — code werd voorspelbaarder, tests stabieler.

ComponentVerantwoordelijkheidPlatform
ModelGegevens, bedrijfslogica, repositoriesAndroid/iOS
ViewWeergave, abonnement op ViewModelActivity/Composable, UIView/SwiftUI View
ViewModelSchermstatus, logica, navigatieViewModel (Jetpack), ObservableObject

Reactieve binding — de basis van MVVM. In Android is LiveData (onderdeel van Jetpack) een waarneembare gegevensopslag. Activity abonneert zich via observe(): viewModel.user.observe(this) { user -> binding.name.text = user.name }. Bij wijziging van user ontvangen alle abonnees automatisch de nieuwe waarde. In iOS gebruikt SwiftUI @Published-eigenschappen in ViewModel — wijzigingen tekenen View automatisch opnieuw. Dit elimineert handmatige aanroepen van showUser/hideLoading die nodig zijn in MVP.

MVVM in Android: ViewModel, LiveData en StateFlow

ViewModel van Jetpack — de officiële component van Google voor het implementeren van MVVM. ViewModel overleeft schermrotatie: bij configuratiewijziging wordt Activity vernietigd en opnieuw aangemaakt, terwijl ViewModel in het geheugen blijft. De nieuwe Activity-instantie ontvangt dezelfde ViewModel via ViewModelProvider. ViewModel heeft geen verwijzingen naar Activity, Context of View — het is zuiver en wordt getest met unittesten zonder Robolectric.

kotlin
// ViewModel met StateFlow — moderne implementatie van MVVM
class UserViewModel(
    private val repository: UserRepository
) : ViewModel() {

    private val _state = MutableStateFlow<UserState>(UserState.Loading)
    val state: StateFlow<UserState> = _state.asStateFlow()

    fun loadUser(userId: Int) {
        viewModelScope.launch {
            _state.value = UserState.Loading
            repository.getUser(userId)
                .onSuccess { user ->
                    _state.value = UserState.Success(user)
                }
                .onFailure { e ->
                    _state.value = UserState.Error(e.message ?: "Unknown")
                }
        }
    }
}

sealed interface UserState {
    data object Loading : UserState
    data class Success(val user: User) : UserState
    data class Error(val message: String) : UserState
}

// View (Activity) abonneert zich op state
class UserActivity : AppCompatActivity() {
    private val viewModel: UserViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        viewModel.state.onEach { state ->
            when (state) {
                is UserState.Loading -> /* laden tonen */
                is UserState.Success -> /* gegevens weergeven */
                is UserState.Error -> /* fout tonen */
            }
        }.launchIn(lifecycleScope)
        viewModel.loadUser(42)
    }
}

LiveData vs StateFlow — LiveData (2017) — de eerste reactieve Jetpack-component, geoptimaliseerd voor de Activity-levenscyclus: automatisch uitschrijven bij onStop. StateFlow (2021) — Kotlin Flow-implementatie, niet gebonden aan de levenscyclus, maar vereist handmatig uitschrijven via lifecycleScope. StateFlow ondersteunt coroutines, concat, map en andere Flow-operatoren die LiveData niet heeft. Bij IT Sectr gebruiken we StateFlow voor alle nieuwe ViewModels — het is korter, krachtiger en integreert beter met coroutines.

DataBinding en ViewBinding — DataBinding verbindt ViewModel met XML via @{viewModel.user.name} rechtstreeks in de layout, waardoor code in Activity wordt geëlimineerd. ViewBinding genereert een type-veilige klasse voor toegang tot View. Google beveelt ViewBinding aan voor eenvoudige projecten en DataBinding voor projecten met complexe gegevensbinding. In Jetpack Compose is DataBinding niet nodig — @Composable functies worden automatisch opnieuw getekend bij wijziging van State.

MVVM in iOS: ObservableObject en SwiftUI

MVVM in iOS wordt geïmplementeerd via ObservableObject uit Combine. ViewModel — een klasse die overerft van ObservableObject, met @Published-velden. SwiftUI View abonneert zich op ViewModel via @ObservedObject of @StateObject. Bij wijziging van een @Published-eigenschap tekent SwiftUI automatisch de View opnieuw die van deze eigenschap afhankelijk is. Apple introduceerde SwiftUI in 2019 op WWDC samen met Combine — vanaf dat moment werd MVVM het officieel aanbevolen patroon voor iOS.

swift
import SwiftUI
import Combine

// ViewModel — ObservableObject met @Published velden
final class UserViewModel: ObservableObject {
    @Published private(set) var state: UserState = .loading
    private let service: UserService

    init(service: UserService) {
        self.service = service
    }

    func loadUser(id: Int) {
        state = .loading
        service.fetchUser(id: id) { [weak self] result in
            guard let self else { return }
            switch result {
            case .success(let user):
                self.state = .success(user)
            case .failure(let error):
                self.state = .error(error.localizedDescription)
            }
        }
    }
}

enum UserState {
    case loading
    case success(User)
    case error(String)
}

// SwiftUI View — abonneert zich op ViewModel
struct UserView: View {
    @StateObject private var viewModel: UserViewModel

    var body: some View {
        switch viewModel.state {
        case .loading:
            ProgressView()
        case .success(let user):
            VStack {
                Text(user.name).font(.title)
                Text(user.email).font(.body)
            }
        case .error(let message):
            Text(message).foregroundColor(.red)
        }
    }
}

@StateObject vs @ObservedObject — @StateObject creëert ViewModel en beheert de levenscyclus ervan (eenmaal tijdens de levensduur van View). @ObservedObject — ViewModel wordt extern gemaakt en doorgegeven aan View. WWDC 2022 beveelt @StateObject aan voor creatie en @ObservedObject voor het doorgeven van ViewModel tussen Views. In iOS 17 (2023) verscheen @Observable — een macro die abonnement automatiseert en @Published-annotaties elimineert. @Observable — de evolutie van Combine, die iOS-ontwikkeling dichter bij de reactiviteit van Kotlin Flow brengt.

UIKit + MVVM — voor projecten op UIKit (zonder SwiftUI) wordt MVVM geïmplementeerd via Combine en @Published met abonnement in UIViewController via sink(). ViewModel is hetzelfde, View — UIViewController met abonnementen op @Published. Combine is beschikbaar sinds iOS 13 (2019) en is ingebouwd in het systeem — vereist geen extra afhankelijkheden. Volgens Apple Developer Survey (2025) gebruikt 45% van de iOS-projecten Combine zelfs met UIKit, 35% gebruikt SwiftUI + Combine, 20% — RxSwift (legacy).

Vergelijking van MVVM met MVP: voor- en nadelen

MVVM wint van MVP in drie belangrijke aspecten: afwezigheid van ViewContract-interfaces, automatisch abonnementbeheer en overleven van schermrotatie. In MVP vereist elk scherm een ViewContract-interface + Presenter-klasse + abonnement/uitschrijven in onStart/onStop. In MVVM wordt alleen ViewModel gemaakt — abonnement in Activity gebeurt via observe() zonder handmatig detach().

CriteriumMVPMVVM
ViewContract-interfaces1 per schermNiet nodig
AbonnementbeheerHandmatig attach/detachAutomatisch (lifecycle-aware)
SchermrotatieRetain-fragmentViewModel overleeft
TestenMock ViewContractZuivere klasse zonder afhankelijkheden
ReactiviteitCallbacks in PresenterLiveData/StateFlow/Combine

Nadelen van MVVM — complexiteit van het debuggen van reactieve ketens en risico op geheugenlekken bij onjuist abonnement. LiveData lost het probleem van levenscyclusveiligheid op, StateFlow vereist lifecycleScope, Combine — sink met AnyCancellable. In MVP zijn alle aanroepen expliciet (view.showUser), in MVVM komen gegevens via een reactieve stroom — tracing vereist debug-breakpoints in de subscribe-closure. In grote ViewModels met meerdere StateFlows kan een UI-update worden gemist als View niet is geabonneerd op een specifieke Flow.

Wanneer MVP nog steeds beter is — in projecten met minimale Android-versie onder API 21 (Android 5), waar Jetpack ViewModel niet beschikbaar is zonder AndroidX, en in projecten op pure UIKit zonder Combine (iOS 12 en lager). Voor legacy-projecten waar de gehele codebase al op MVP is, is volledige overgang naar MVVM niet altijd gerechtvaardigd — het is goedkoper om MVP te onderhouden met geleidelijke verplaatsing van logica naar services, dan 100 schermen in 3 maanden te herschrijven.

ViewModel testen op Android en iOS

ViewModel wordt getest met unittesten zonder platformafhankelijkheden — dit is het belangrijkste argument voor MVVM. Op Android bevat ViewModel geen Activity, Context of View — alle afhankelijkheden (Repository, UseCase) worden via de constructor doorgegeven en vervangen door mock-objecten. Op iOS wordt ObservableObject getest via XCTest zonder de app te starten, wat stabiliteit en snelheid van testuitvoering biedt.

kotlin
// Unittest Android ViewModel met MockK
class UserViewModelTest {

    private val repository = mockk<UserRepository>()
    private val viewModel = UserViewModel(repository)

    @Test
    fun loadUser_success_updatesState() = runTest {
        val user = User(1, "John", "john@test.com")
        coEvery { repository.getUser(1) } returns Result.success(user)

        viewModel.loadUser(1)

        assertEquals(UserState.Success(user), viewModel.state.value)
    }

    @Test
    fun loadUser_error_updatesErrorState() = runTest {
        val error = RuntimeException("Network error")
        coEvery { repository.getUser(1) } returns Result.failure(error)

        viewModel.loadUser(1)

        val state = viewModel.state.value
        assertTrue(state is UserState.Error)
        assertEquals("Network error", (state as UserState.Error).message)
    }
}

iOS ViewModel wordt op vergelijkbare wijze getest: we injecteren mock UserService, roepen loadUser aan, controleren de status via XCTestExpectation. Combine Publisher wordt getest via XCTestCase met wait(for: expectations, timeout: 1.0). De UserState-structuur — enum met associated values — maakt het mogelijk de exacte schermstatus na een operatie te controleren.

Code dekking in IT Sectr-projecten op MVVM is 75-90% voor ViewModel en Repository. ViewModel wordt gedekt door unittesten, Repository — door integratietesten met een testdatabase. View in SwiftUI en Jetpack Compose wordt getest met UI-tests (XCUITest, Compose Test) voor kritieke scenario's. De rest van UI wordt gecontroleerd met screenshot-tests (Snapshot Testing) — dit is sneller dan UI-tests en geeft 95% vertrouwen in de correctheid van weergave.

Veelgestelde vragen

Wat is het belangrijkste verschil tussen MVVM en MVP?

In MVVM heeft ViewModel geen verwijzing naar View — gegevens worden verzonden via reactieve mechanismen (LiveData, StateFlow, @Published). In MVP roept Presenter rechtstreeks View-methoden aan via de ViewContract-interface. MVVM elimineert ViewContract en handmatig attach/detach, maar vereist begrip van reactieve stromen. ViewModel overleeft schermrotatie op Android, Presenter vereist retain-fragment.

Welke bibliotheken zijn nodig voor MVVM op Android?

Minimale set: lifecycle-viewmodel-ktx (ViewModel), lifecycle-livedata-ktx of kotlinx-coroutines-core (StateFlow). Voor injectie — Hilt of Koin. Voor asynchrone operaties — Kotlin Coroutines. Voor complexe gegevensbinding — DataBinding. In Jetpack Compose (aanbevolen door Google sinds 2022) zijn compose-runtime en lifecycle-viewmodel-compose voldoende.

Waarom beveelt Apple MVVM aan voor iOS?

SwiftUI (2019) is ontworpen voor reactieve architectuur: @State en @Published tekenen View automatisch opnieuw bij gegevenswijziging. MVVM — de natuurlijke match voor SwiftUI: View — @ViewBuilder, ViewModel — ObservableObject. Apple dwingt MVVM niet af als enig patroon, maar alle trainingsmaterialen sinds 2019 gebruiken ViewModel + SwiftUI. Voor UIKit beveelt Apple MVC of Coordinator aan.

Hoe geheugenlekken in ViewModel voorkomen?

Android: viewModelScope annuleert automatisch coroutines bij het opschonen van ViewModel. iOS: AnyCancellable uit Combine schrijft zich automatisch uit bij vrijgave van het object dat het bewaart. SwiftUI @StateObject beheert de levenscyclus automatisch. Hoofdregels: bewaar geen verwijzingen naar View/Context in ViewModel, annuleer langlopende operaties bij opschoning, gebruik weak self in closures.

Wat kiezen: LiveData of StateFlow voor Android?

StateFlow — de moderne keuze. LiveData is eenvoudiger en levenscyclus-veilig, maar StateFlow is krachtiger: werkt met coroutines, ondersteunt flatMap, combine, filter, vereist geen @Nullable-annotatie. Het enige scenario waar LiveData de voorkeur heeft — werken met Java-code waar StateFlow (Kotlin Flow-API) niet beschikbaar is. Google beveelt StateFlow aan voor nieuwe projecten in Kotlin.

Samenvatting

  • MVVM (Model-View-ViewModel) — reactief patroon waarin ViewModel geen verwijzing naar View heeft
  • ViewModel — overleeft schermrotatie, getest met unittesten, onafhankelijk van UI
  • Android — ViewModel + StateFlow + Kotlin Coroutines — moderne stack van Google
  • iOS — ObservableObject + @Published + SwiftUI — native implementatie van MVVM
  • MVVM vs MVP — MVVM elimineert ViewContract en handmatig attach/detach
  • Testen — ViewModel gedekt door unittesten zonder platformafhankelijkheden
  • Aanbeveling — MVVM voor nieuwe projecten; MVP voor legacy-ondersteuning

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