MVVM (Model-View-ViewModel) ist ein Architekturmuster, bei dem ViewModel den Presenter ersetzt und reaktive Mechanismen zur Kommunikation mit der View verwendet: ObservableObject in SwiftUI, LiveData/StateFlow in Android. ViewModel hat keine Referenz auf die View — Daten werden über Abonnement übergeben, wodurch ViewContract-Schnittstellen überflüssig werden und das Testen noch einfacher wird. Apple empfiehlt MVVM mit SwiftUI seit 2019, Google empfiehlt MVVM mit Jetpack als offizielle Android-Architektur. Weitere Informationen finden Sie im Android Architecture Guide.
Wichtigste Erkenntnisse
MVVM (Model-View-ViewModel) ist ein Architekturmuster, das von John Gossman im Jahr 2005 für Windows Presentation Foundation (WPF) von Microsoft beschrieben wurde. ViewModel ist die zentrale Komponente, die den Bildschirmzustand und die Geschäftslogik enthält, aber keine Referenz auf die View hat. Daten werden über reaktive Bindungsmechanismen übergeben: Die View abonniert Änderungen des ViewModels und wird bei Datenänderungen automatisch neu gerendert.
Hauptunterschied zwischen MVVM und MVP — Fehlen von ViewContract. In MVP ruft der Presenter Methoden wie view.showUser(data) auf, d.h. der Presenter "schiebt" Daten aktiv in die View. In MVVM "zieht" die View selbst Daten aus dem ViewModel über ein Abonnement: Das ViewModel weiß nicht, ob es einen Abonnenten hat. Dies beseitigt das Problem der getrennten View — wenn die Activity bei Drehung zerstört wird, arbeitet das ViewModel weiter, und die neue Activity abonniert einfach die aktuellen Daten. Bei IT Sectr verwenden wir seit 2020 MVVM in allen neuen Projekten — der Code ist vorhersagbarer geworden, die Tests stabiler.
| Komponente | Verantwortung | Plattform |
|---|---|---|
| Model | Daten, Geschäftslogik, Repositories | Android/iOS |
| View | Anzeige, Abonnement des ViewModels | Activity/Composable, UIView/SwiftUI View |
| ViewModel | Bildschirmzustand, Logik, Navigation | ViewModel (Jetpack), ObservableObject |
Reaktive Bindung — die Grundlage von MVVM. In Android ist LiveData (Teil von Jetpack) ein beobachtbarer Datenhalter. Die Activity abonniert über observe(): viewModel.user.observe(this) { user -> binding.name.text = user.name }. Wenn sich user ändert, erhalten alle Abonnenten automatisch den neuen Wert. In iOS verwendet SwiftUI @Published-Eigenschaften im ViewModel — Änderungen rendern die View automatisch neu. Dies eliminiert manuelle showUser/hideLoading-Aufrufe, die in MVP erforderlich sind.
ViewModel von Jetpack — die offizielle Komponente von Google zur Implementierung von MVVM. ViewModel überlebt die Bildschirmdrehung: Bei Konfigurationsänderungen wird die Activity zerstört und neu erstellt, während das ViewModel im Speicher bleibt. Die neue Activity-Instanz erhält dasselbe ViewModel über ViewModelProvider. Das ViewModel hat keine Referenzen auf Activity, Context oder View — es ist sauber und mit Unit-Tests ohne Robolectric testbar.
// ViewModel mit StateFlow — moderne MVVM-Implementierung
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) abonniert 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 -> /* Ladevorgang anzeigen */
is UserState.Success -> /* Daten anzeigen */
is UserState.Error -> /* Fehler anzeigen */
}
}.launchIn(lifecycleScope)
viewModel.loadUser(42)
}
}
LiveData vs StateFlow — LiveData (2017) — die erste reaktive Komponente von Jetpack, optimiert für den Activity-Lebenszyklus: automatische Abbestellung bei onStop. StateFlow (2021) — Kotlin Flow-Implementierung, nicht an den Lebenszyklus gebunden, erfordert jedoch manuelle Abbestellung über lifecycleScope. StateFlow unterstützt Coroutinen, concat, map und andere Flow-Operatoren, die LiveData nicht hat. Bei IT Sectr verwenden wir StateFlow für alle neuen ViewModels — es ist kürzer, leistungsfähiger und lässt sich besser mit Coroutinen integrieren.
DataBinding und ViewBinding — DataBinding bindet das ViewModel an XML über @{viewModel.user.name} direkt im Layout und eliminiert Code in der Activity. ViewBinding generiert eine typsichere Klasse für den Zugriff auf Views. Google empfiehlt ViewBinding für einfache Projekte und DataBinding für Projekte mit komplexer Datenbindung. In Jetpack Compose ist DataBinding nicht erforderlich — @Composable-Funktionen werden bei State-Änderungen automatisch neu gerendert.
MVVM in iOS wird über ObservableObject aus Combine implementiert. ViewModel ist eine Klasse, die ObservableObject erbt, mit @Published-Eigenschaften. Die SwiftUI View abonniert das ViewModel über @ObservedObject oder @StateObject. Wenn sich eine @Published-Eigenschaft ändert, rendert SwiftUI automatisch die View neu, die von dieser Eigenschaft abhängt. Apple stellte SwiftUI 2019 auf der WWDC zusammen mit Combine vor — seitdem ist MVVM das offiziell empfohlene Muster für iOS.
import SwiftUI
import Combine
// ViewModel — ObservableObject mit @Published-Feldern
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 — abonniert 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 erstellt das ViewModel und verwaltet seinen Lebenszyklus (einmal pro View-Lebensdauer). @ObservedObject — das ViewModel wird extern erstellt und an die View übergeben. Die WWDC 2022 empfiehlt @StateObject zur Erstellung und @ObservedObject zur Übergabe des ViewModels zwischen Views. In iOS 17 (2023) erschien das @Observable-Makro — es automatisiert das Abonnement und macht @Published-Annotationen überflüssig. @Observable ist die Weiterentwicklung von Combine und bringt die iOS-Entwicklung näher an die Kotlin-Flow-Reaktivität.
UIKit + MVVM — für UIKit-Projekte (ohne SwiftUI) wird MVVM über Combine und @Published mit Abonnement im UIViewController über sink() implementiert. Das ViewModel bleibt gleich, die View ist ein UIViewController mit Abonnements auf @Published. Combine ist seit iOS 13 (2019) verfügbar und systemintegriert — keine zusätzlichen Abhängigkeiten erforderlich. Laut Apple Developer Survey (2025) verwenden 45% der iOS-Projekte Combine auch mit UIKit, 35% verwenden SwiftUI + Combine, 20% verwenden RxSwift (Legacy).
MVVM gewinnt gegenüber MVP in drei Schlüsselaspekten: Fehlen von ViewContract-Schnittstellen, automatische Abonnementverwaltung und Überleben der Bildschirmdrehung. In MVP erfordert jeder Bildschirm eine ViewContract-Schnittstelle + Presenter-Klasse + Abonnement/Kündigung in onStart/onStop. In MVVM wird nur das ViewModel erstellt — das Abonnement in der Activity erfolgt über observe() ohne manuelles detach().
| Kriterium | MVP | MVVM |
|---|---|---|
| ViewContract-Schnittstellen | 1 pro Bildschirm | Nicht erforderlich |
| Abonnementverwaltung | Manuelles attach/detach | Automatisch (lifecycle-aware) |
| Bildschirmdrehung | Retain-Fragment | ViewModel überlebt Drehung |
| Testen | Mock ViewContract | Saubere Klasse ohne Abhängigkeiten |
| Reaktivität | Callbacks im Presenter | LiveData/StateFlow/Combine |
Nachteile von MVVM — Komplexität beim Debuggen reaktiver Ketten und Risiko von Speicherlecks bei falschem Abonnement. LiveData löst die Lebenszyklussicherheit, StateFlow erfordert lifecycleScope, Combine erfordert sink mit AnyCancellable. In MVP sind alle Aufrufe explizit (view.showUser), in MVVM kommen Daten über einen reaktiven Stream — die Rückverfolgung erfordert Debug-Breakpoints in subscribe-Closures. In großen ViewModels mit mehreren StateFlows kann ein UI-Update verpasst werden, wenn die View ein bestimmtes Flow nicht abonniert hat.
Wann MVP immer noch besser ist — in Projekten mit einer Mindest-Android-Version unter API 21 (Android 5), wo Jetpack ViewModel ohne AndroidX nicht verfügbar ist, und in Projekten mit reinem UIKit ohne Combine (iOS 12 und niedriger). Für Legacy-Projekte, deren gesamte Codebasis bereits auf MVP basiert, ist ein vollständiger Übergang zu MVVM nicht immer gerechtfertigt — es ist günstiger, MVP mit schrittweiser Auslagerung der Logik in Dienste zu pflegen, als 100 Bildschirme in 3 Monaten neu zu schreiben.
ViewModel wird mit Unit-Tests getestet ohne Plattformabhängigkeiten — das ist das Hauptargument für MVVM. Auf Android enthält das ViewModel keine Activity, keinen Context und keine View — alle Abhängigkeiten (Repository, UseCase) werden über den Konstruktor übergeben und durch Mock-Objekte ersetzt. Auf iOS wird ObservableObject über XCTest ohne App-Start getestet, was Stabilität und Testgeschwindigkeit bietet.
// Android ViewModel-Unit-Test mit 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 wird ähnlich getestet: Mock UserService injizieren, loadUser aufrufen, Zustand über XCTestExpectation prüfen. Combine Publisher wird über XCTestCase mit wait(for: expectations, timeout: 1.0) getestet. Die UserState-Struktur — ein Enum mit assoziierten Werten — ermöglicht die Überprüfung des genauen Bildschirmzustands nach einer Operation.
Codeabdeckung in IT Sectr-Projekten mit MVVM beträgt 75–90% für ViewModel und Repository. ViewModel wird durch Unit-Tests abgedeckt, Repository durch Integrationstests mit einer Testdatenbank. View in SwiftUI und Jetpack Compose wird für kritische Szenarien mit UI-Tests (XCUITest, Compose Test) getestet. Die restliche UI wird mit Screenshot-Tests (Snapshot Testing) überprüft — das ist schneller als UI-Tests und bietet 95% Sicherheit in der korrekten Darstellung.
Häufig gestellte Fragen
In MVVM hat das ViewModel keine Referenz auf die View — Daten werden über reaktive Mechanismen (LiveData, StateFlow, @Published) übergeben. In MVP ruft der Presenter direkt View-Methoden über die ViewContract-Schnittstelle auf. MVVM eliminiert ViewContract und manuelles attach/detach, erfordert jedoch Verständnis reaktiver Streams. Das ViewModel überlebt die Bildschirmdrehung auf Android, der Presenter benötigt ein retain-Fragment.
Mindestsatz: lifecycle-viewmodel-ktx (ViewModel), lifecycle-livedata-ktx oder kotlinx-coroutines-core (StateFlow). Für Injektion — Hilt oder Koin. Für asynchrone Operationen — Kotlin Coroutines. Für komplexe Datenbindung — DataBinding. In Jetpack Compose (von Google seit 2022 empfohlen) sind compose-runtime und lifecycle-viewmodel-compose ausreichend.
SwiftUI (2019) ist für reaktive Architektur konzipiert: @State und @Published rendern die View bei Datenänderungen automatisch neu. MVVM passt natürlich zu SwiftUI: View — @ViewBuilder, ViewModel — ObservableObject. Apple erzwingt MVVM nicht als einziges Muster, aber alle Schulungsmaterialien seit 2019 verwenden ViewModel + SwiftUI. Für UIKit empfiehlt Apple MVC oder Coordinator.
Android: viewModelScope bricht Coroutinen beim Löschen des ViewModels automatisch ab. iOS: AnyCancellable aus Combine kündigt das Abonnement automatisch beim Freigeben des haltenden Objekts. SwiftUI @StateObject verwaltet den Lebenszyklus automatisch. Hauptregeln: Keine Referenzen auf View/Context im ViewModel speichern, langlaufende Operationen beim Bereinigen abbrechen, weak self in Closures verwenden.
StateFlow ist die moderne Wahl. LiveData ist einfacher und lebenszyklussicher, aber StateFlow ist leistungsfähiger: arbeitet mit Coroutinen, unterstützt flatMap, combine, filter, benötigt keine @Nullable-Annotation. Das einzige Szenario, in dem LiveData vorzuziehen ist — die Arbeit mit Java-Code, wo StateFlow (Kotlin Flow API) nicht verfügbar ist. Google empfiehlt StateFlow für neue Kotlin-Projekte.
Zusammenfassung
Wir entwickeln eine mobile Applikation schlüsselfertig
IT Sectr entwickelt seit 2017 iOS- und Android-Apps für Startups und Unternehmen. Wir beraten Sie und schlagen die beste Lösung vor.
Lesen Sie auch