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-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.
| Component | Verantwoordelijkheid | Platform |
|---|---|---|
| Model | Gegevens, bedrijfslogica, repositories | Android/iOS |
| View | Weergave, abonnement op ViewModel | Activity/Composable, UIView/SwiftUI View |
| ViewModel | Schermstatus, logica, navigatie | ViewModel (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.
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.
// 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 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.
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).
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().
| Criterium | MVP | MVVM |
|---|---|---|
| ViewContract-interfaces | 1 per scherm | Niet nodig |
| Abonnementbeheer | Handmatig attach/detach | Automatisch (lifecycle-aware) |
| Schermrotatie | Retain-fragment | ViewModel overleeft |
| Testen | Mock ViewContract | Zuivere klasse zonder afhankelijkheden |
| Reactiviteit | Callbacks in Presenter | LiveData/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 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.
// 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
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.
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.
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.
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.
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
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