MVVM vs MVI in Android: Welche Architektur in 2026 wählen?
Ausführlicher Vergleich von MVVM und MVI in Android: Vorteile, Einschränkungen, Anwendungsfälle und ein praxisnaher Leitfaden zur Architekturwahl in 2026.

Die Wahl der richtigen Architektur ist eine entscheidende Entscheidung, die die Wartbarkeit, Testbarkeit und Skalierbarkeit einer Android-Anwendung beeinflusst. Im Jahr 2026 dominieren zwei Muster das Ökosystem: MVVM, der Industriestandard, und MVI, der reaktive Ansatz, der mit Jetpack Compose zunehmend an Bedeutung gewinnt.
Eine schlechte Architekturwahl ist teuer: technische Schulden, schwer reproduzierbare Fehler und schmerzhafte Refaktorierungen. Das Verständnis der Stärken und Schwächen jedes Ansatzes erspart langfristig erhebliche Probleme.
MVVM verstehen: Der etablierte Standard
MVVM (Model-View-ViewModel) ist Googles empfohlene Architektur seit der Einführung von Jetpack. Sie trennt Verantwortlichkeiten sauber in drei Schichten und macht den Code übersichtlicher und testbarer.
MVVM-Grundprinzipien
Das MVVM-Muster basiert auf einer klaren Trennung: Das Model verwaltet Daten und Geschäftslogik, die View zeigt die Oberfläche an, und das ViewModel verbindet beide, indem es beobachtbare Zustände bereitstellt.
Im Folgenden wird ein Benutzerprofil-Bildschirm mit MVVM implementiert. Dieses erste Beispiel zeigt die grundlegende Struktur mit einem ViewModel, das beobachtbaren Zustand und Methoden für Benutzerinteraktionen bereitstellt.
// Classic MVVM ViewModel for a user profile screen
// It exposes observable state and methods for actions
class UserProfileViewModel(
private val userRepository: UserRepository,
private val analyticsTracker: AnalyticsTracker
) : ViewModel() {
// Observable state with StateFlow - the View observes these changes
private val _uiState = MutableStateFlow(UserProfileState())
val uiState: StateFlow<UserProfileState> = _uiState.asStateFlow()
// Separate loading state - MVVM allows multiple flows
private val _isLoading = MutableStateFlow(false)
val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()
// One-shot error messages
private val _errorMessage = MutableSharedFlow<String>()
val errorMessage: SharedFlow<String> = _errorMessage.asSharedFlow()
// Initial profile loading
fun loadProfile(userId: String) {
viewModelScope.launch {
_isLoading.value = true
try {
// Repository call to fetch data
val user = userRepository.getUser(userId)
// Update state with new data
_uiState.update { currentState ->
currentState.copy(
user = user,
isEditing = false
)
}
// Analytics tracking
analyticsTracker.trackProfileViewed(userId)
} catch (e: Exception) {
// Emit one-shot error message
_errorMessage.emit("Unable to load profile")
} finally {
_isLoading.value = false
}
}
}
// Enable edit mode
fun enableEditMode() {
_uiState.update { it.copy(isEditing = true) }
}
// Save profile changes
fun saveProfile(name: String, bio: String) {
viewModelScope.launch {
_isLoading.value = true
try {
val updatedUser = userRepository.updateUser(
_uiState.value.user?.id ?: return@launch,
name = name,
bio = bio
)
_uiState.update {
it.copy(user = updatedUser, isEditing = false)
}
} catch (e: Exception) {
_errorMessage.emit("Failed to save profile")
} finally {
_isLoading.value = false
}
}
}
}
// Data class representing the screen state
data class UserProfileState(
val user: User? = null,
val isEditing: Boolean = false
)Dieses ViewModel veranschaulicht den typischen MVVM-Ansatz: mehrere beobachtbare Flows (Hauptzustand, Laden, Fehler) und öffentliche Methoden für jede Benutzeraktion.
Vorteile von MVVM
MVVM hat mehrere Stärken, die seine breite Adoption erklären:
- Bekanntheit: Die meisten Android-Entwickler kennen dieses Muster
- Flexibilität: Der Zustand kann beliebig strukturiert werden
- Ökosystem: Perfekte Integration mit Jetpack (LiveData, StateFlow, Hilt)
- Einfachheit: Flache Lernkurve für Einsteiger
MVVM eignet sich besonders für gemischte Teams mit Entwicklern unterschiedlicher Erfahrungsstufen. Die konzeptionelle Einfachheit erleichtert das Onboarding.
Einschränkungen von MVVM
MVVM zeigt jedoch seine Grenzen, wenn die Anwendung wächst. Das Hauptproblem ist das verteilte Zustandsmanagement. Das folgende Beispiel veranschaulicht dieses häufige Problem:
// Example MVVM ViewModel with fragmented state
// This pattern becomes problematic as the screen grows in complexity
class CheckoutViewModel : ViewModel() {
// Problem: state scattered across multiple flows
private val _cart = MutableStateFlow<List<CartItem>>(emptyList())
private val _selectedAddress = MutableStateFlow<Address?>(null)
private val _selectedPayment = MutableStateFlow<PaymentMethod?>(null)
private val _promoCode = MutableStateFlow<String?>(null)
private val _isLoading = MutableStateFlow(false)
private val _error = MutableStateFlow<String?>(null)
// Each modification can create temporary inconsistent states
fun applyPromoCode(code: String) {
viewModelScope.launch {
_isLoading.value = true
_error.value = null // Reset error
try {
val discount = promoRepository.validate(code)
_promoCode.value = code
// Cart state also needs updating...
// but there's a delay between the two updates
recalculateCart()
} catch (e: Exception) {
_error.value = e.message
_promoCode.value = null
} finally {
_isLoading.value = false
}
}
}
// Hard to guarantee consistency across all these states
private fun recalculateCart() {
// Complex logic depending on multiple states...
}
}Dieses Beispiel zeigt, wie der Zustand in MVVM fragmentiert werden kann, was das Nachverfolgen von Übergängen und das Reproduzieren von Fehlern erschwert.
MVI verstehen: Der unidirektionale Ansatz
MVI (Model-View-Intent) verfolgt eine andere Philosophie: unidirektionaler Datenfluss und ein einzelner unveränderlicher Zustand. Dieser von Redux inspirierte Ansatz eliminiert Probleme mit inkonsistentem Zustand.
MVI-Grundprinzipien
In MVI folgt alles einem klaren Zyklus: Der Benutzer sendet einen Intent (Aktion), der Reducer transformiert den aktuellen Zustand in einen neuen, und die View zeigt diesen einzelnen Zustand an. Das ist vorhersehbar, testbar und debuggbar.
Im Folgenden wird derselbe Benutzerprofil-Bildschirm mit MVI implementiert. Dabei fällt auf, wie der Zustand zentralisiert und die Aktionen explizit typisiert sind.
// MVI ViewModel for the same user profile screen
// Note the structure: Intent → Reducer → Single State
class UserProfileMviViewModel(
private val userRepository: UserRepository,
private val analyticsTracker: AnalyticsTracker
) : ViewModel() {
// Single, immutable state - the absolute source of truth
private val _state = MutableStateFlow(UserProfileState())
val state: StateFlow<UserProfileState> = _state.asStateFlow()
// Channel for side effects (navigation, snackbar)
private val _sideEffect = Channel<UserProfileSideEffect>()
val sideEffect: Flow<UserProfileSideEffect> = _sideEffect.receiveAsFlow()
// Single entry point for all user actions
fun onIntent(intent: UserProfileIntent) {
when (intent) {
is UserProfileIntent.LoadProfile -> loadProfile(intent.userId)
is UserProfileIntent.EnableEditMode -> enableEditMode()
is UserProfileIntent.SaveProfile -> saveProfile(intent.name, intent.bio)
is UserProfileIntent.CancelEdit -> cancelEdit()
}
}
private fun loadProfile(userId: String) {
viewModelScope.launch {
// Transition to loading state
_state.update { it.copy(isLoading = true, error = null) }
try {
val user = userRepository.getUser(userId)
// Single atomic state update
_state.update {
it.copy(
user = user,
isLoading = false,
error = null
)
}
analyticsTracker.trackProfileViewed(userId)
} catch (e: Exception) {
// Error state is part of the main state
_state.update {
it.copy(
isLoading = false,
error = "Unable to load profile"
)
}
}
}
}
private fun enableEditMode() {
// Simple, predictable update
_state.update { it.copy(isEditing = true) }
}
private fun saveProfile(name: String, bio: String) {
viewModelScope.launch {
val currentUser = _state.value.user ?: return@launch
_state.update { it.copy(isLoading = true) }
try {
val updatedUser = userRepository.updateUser(
currentUser.id,
name = name,
bio = bio
)
_state.update {
it.copy(
user = updatedUser,
isEditing = false,
isLoading = false
)
}
// Side effect to notify the user
_sideEffect.send(UserProfileSideEffect.ShowSuccess("Profile updated"))
} catch (e: Exception) {
_state.update {
it.copy(isLoading = false, error = "Failed to save profile")
}
}
}
}
private fun cancelEdit() {
_state.update { it.copy(isEditing = false) }
}
}
// All possible actions, explicitly typed
sealed class UserProfileIntent {
data class LoadProfile(val userId: String) : UserProfileIntent()
object EnableEditMode : UserProfileIntent()
data class SaveProfile(val name: String, val bio: String) : UserProfileIntent()
object CancelEdit : UserProfileIntent()
}
// Single, complete screen state
data class UserProfileState(
val user: User? = null,
val isLoading: Boolean = false,
val isEditing: Boolean = false,
val error: String? = null
)
// One-shot side effects
sealed class UserProfileSideEffect {
data class ShowSuccess(val message: String) : UserProfileSideEffect()
data class NavigateTo(val destination: String) : UserProfileSideEffect()
}Der Unterschied ist klar: ein einzelner Zustandsflow, explizite Aktionen und eine saubere Trennung zwischen persistentem Zustand und einmaligen Effekten.
Mit MVI lässt sich jeder Intent und jeder Zustandsübergang protokollieren. Einen Fehler zu reproduzieren wird trivial: Einfach die Abfolge der Intents wiederholen.
MVI mit Jetpack Compose
MVI glänzt besonders mit Jetpack Compose, da beide dieselbe Philosophie teilen: unveränderlicher Zustand und deklarative UI. So wird das ViewModel mit einem Compose-Screen verbunden:
// Compose screen consuming MVI state
// The connection between ViewModel and UI is elegant and reactive
@Composable
fun UserProfileScreen(
viewModel: UserProfileMviViewModel = hiltViewModel(),
onNavigateBack: () -> Unit
) {
// Collect the single state
val state by viewModel.state.collectAsStateWithLifecycle()
// Handle side effects
LaunchedEffect(Unit) {
viewModel.sideEffect.collect { effect ->
when (effect) {
is UserProfileSideEffect.ShowSuccess -> {
// Show snackbar
}
is UserProfileSideEffect.NavigateTo -> {
// Navigate
}
}
}
}
// Purely declarative UI based on state
UserProfileContent(
state = state,
onIntent = viewModel::onIntent
)
}
@Composable
private fun UserProfileContent(
state: UserProfileState,
onIntent: (UserProfileIntent) -> Unit
) {
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
// Conditional rendering based on the single state
when {
state.isLoading -> {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.CenterHorizontally)
)
}
state.error != null -> {
ErrorMessage(
message = state.error,
onRetry = {
state.user?.id?.let {
onIntent(UserProfileIntent.LoadProfile(it))
}
}
)
}
state.user != null -> {
ProfileCard(
user = state.user,
isEditing = state.isEditing,
onEditClick = { onIntent(UserProfileIntent.EnableEditMode) },
onSaveClick = { name, bio ->
onIntent(UserProfileIntent.SaveProfile(name, bio))
},
onCancelClick = { onIntent(UserProfileIntent.CancelEdit) }
)
}
}
}
}Die UI wird zur reinen Funktion des Zustands: vorhersehbar, testbar und ohne versteckte Seiteneffekte.
Detaillierter Vergleich
Nachdem beide Muster in der Praxis gesehen wurden, werden sie anhand der Kriterien verglichen, die in der Produktion wirklich wichtig sind.
Zustandsverwaltung
Der grundlegende Unterschied liegt in der Zustandsverwaltung. Diese Unterscheidung beeinflusst direkt die langfristige Wartbarkeit.
// MVVM: potentially fragmented state
class MvvmViewModel : ViewModel() {
// Multiple sources of truth - manual synchronization needed
private val _users = MutableStateFlow<List<User>>(emptyList())
private val _selectedUser = MutableStateFlow<User?>(null)
private val _isLoading = MutableStateFlow(false)
private val _searchQuery = MutableStateFlow("")
// What happens if _selectedUser points to a user
// that's no longer in _users after a refresh?
// → Inconsistent state that's hard to detect
}
// MVI: consistent state by construction
class MviViewModel : ViewModel() {
// Single source of truth - inconsistencies are impossible
private val _state = MutableStateFlow(UsersState())
data class UsersState(
val users: List<User> = emptyList(),
val selectedUser: User? = null, // Always consistent with users
val isLoading: Boolean = false,
val searchQuery: String = ""
)
// Each update automatically maintains invariants
private fun selectUser(userId: String) {
_state.update { currentState ->
currentState.copy(
selectedUser = currentState.users.find { it.id == userId }
)
}
}
}In MVVM manifestieren sich inkonsistente Zustände oft als intermittierende Fehler, die schwer zu reproduzieren sind. In MVI ist ein ungültiger Zustand deterministisch ungültig.
Testbarkeit der Architektur
Beide Architekturen sind testbar, aber MVI bietet durch seine Vorhersehbarkeit einen wesentlichen Vorteil.
// MVVM test: requires verifying multiple flows
@Test
fun `loadUsers should update state correctly`() = runTest {
val viewModel = MvvmViewModel(fakeRepository)
// Observe multiple flows simultaneously
val users = mutableListOf<List<User>>()
val loadingStates = mutableListOf<Boolean>()
val job1 = launch { viewModel.users.toList(users) }
val job2 = launch { viewModel.isLoading.toList(loadingStates) }
viewModel.loadUsers()
advanceUntilIdle()
// Assertions on different flows
assertThat(users.last()).isEqualTo(expectedUsers)
assertThat(loadingStates).containsExactly(false, true, false)
job1.cancel()
job2.cancel()
}
// MVI test: single flow to verify, clear state sequence
@Test
fun `LoadUsers intent should produce correct state sequence`() = runTest {
val viewModel = MviViewModel(fakeRepository)
// Collect all states in order
val states = mutableListOf<UsersState>()
val job = launch { viewModel.state.toList(states) }
// Send the intent
viewModel.onIntent(UsersIntent.LoadUsers)
advanceUntilIdle()
// Verify the exact state sequence
assertThat(states).containsExactly(
UsersState(), // Initial
UsersState(isLoading = true), // Loading
UsersState(users = expectedUsers, isLoading = false) // Success
)
job.cancel()
}MVI ermöglicht es, die genaue Abfolge der Zustandsübergänge zu testen, was besonders nützlich für komplexe Screens mit vielen Interaktionen ist.
Komplexität und Boilerplate
Die Kompromisse sollten offen benannt werden. MVI erfordert mehr Boilerplate-Code und ein tieferes Verständnis der Konzepte.
// MVVM: quick start, less code
class SimpleViewModel : ViewModel() {
private val _name = MutableStateFlow("")
val name: StateFlow<String> = _name.asStateFlow()
fun updateName(newName: String) {
_name.value = newName
}
}
// Total: ~10 lines
// MVI: more structure, more code
class SimpleMviViewModel : ViewModel() {
private val _state = MutableStateFlow(SimpleState())
val state: StateFlow<SimpleState> = _state.asStateFlow()
fun onIntent(intent: SimpleIntent) {
when (intent) {
is SimpleIntent.UpdateName -> {
_state.update { it.copy(name = intent.name) }
}
}
}
}
data class SimpleState(val name: String = "")
sealed class SimpleIntent {
data class UpdateName(val name: String) : SimpleIntent()
}
// Total: ~20 linesFür einen einfachen Screen kann MVI übertrieben erscheinen. Aber diese Struktur zahlt sich aus, wenn der Screen an Komplexität gewinnt.
Bereit für deine Android-Interviews?
Übe mit unseren interaktiven Simulatoren, Flashcards und technischen Tests.
Wann sollte MVVM gewählt werden?
MVVM bleibt in mehreren Situationen die pragmatische Wahl:
Bestehende Projekte
Wenn die Anwendung bereits MVVM verwendet, ist die Migration zu MVI mit erheblichem Aufwand verbunden. Die Verbesserung der bestehenden MVVM-Struktur ist oft die sinnvollere Entscheidung.
Junior- oder gemischte Teams
MVVM ist zugänglicher. Ein Team mit Einsteigern wird mit MVVM schneller produktiv als mit MVI.
Einfache Screens
Für Screens mit wenigen Zuständen und Interaktionen fügt MVI Komplexität ohne proportionalen Nutzen hinzu.
// For a simple settings screen, MVVM is plenty
class SettingsViewModel(
private val preferencesRepository: PreferencesRepository
) : ViewModel() {
val darkMode = preferencesRepository.darkModeFlow
.stateIn(viewModelScope, SharingStarted.Lazily, false)
val notificationsEnabled = preferencesRepository.notificationsFlow
.stateIn(viewModelScope, SharingStarted.Lazily, true)
fun toggleDarkMode() {
viewModelScope.launch {
preferencesRepository.setDarkMode(!darkMode.value)
}
}
fun toggleNotifications() {
viewModelScope.launch {
preferencesRepository.setNotifications(!notificationsEnabled.value)
}
}
}Wann sollte MVI gewählt werden?
MVI zeigt seinen Wert in bestimmten Kontexten:
Anwendungen mit komplexem Zustand
Wenn ein Screen viele voneinander abhängige Zustände hat, gewährleistet MVI die Konsistenz.
// Checkout screen with complex state: MVI excels
data class CheckoutState(
val cartItems: List<CartItem> = emptyList(),
val selectedAddress: Address? = null,
val selectedPayment: PaymentMethod? = null,
val promoCode: PromoCode? = null,
val deliveryOptions: List<DeliveryOption> = emptyList(),
val selectedDelivery: DeliveryOption? = null,
val subtotal: Money = Money.ZERO,
val discount: Money = Money.ZERO,
val deliveryFee: Money = Money.ZERO,
val total: Money = Money.ZERO,
val isLoading: Boolean = false,
val error: CheckoutError? = null,
val step: CheckoutStep = CheckoutStep.CART
) {
// Verifiable invariants
init {
require(total == subtotal - discount + deliveryFee) {
"Total inconsistent with components"
}
}
}
sealed class CheckoutIntent {
data class AddItem(val item: CartItem) : CheckoutIntent()
data class RemoveItem(val itemId: String) : CheckoutIntent()
data class SelectAddress(val address: Address) : CheckoutIntent()
data class SelectPayment(val method: PaymentMethod) : CheckoutIntent()
data class ApplyPromo(val code: String) : CheckoutIntent()
object RemovePromo : CheckoutIntent()
data class SelectDelivery(val option: DeliveryOption) : CheckoutIntent()
object ProceedToPayment : CheckoutIntent()
object ConfirmOrder : CheckoutIntent()
}Echtzeit-Anwendungen
Für Apps mit WebSockets, Push-Benachrichtigungen oder Echtzeit-Synchronisierung verwaltet MVI mehrere Datenströme elegant.
Strenge Debugging-Anforderungen
In regulierten Bereichen (Fintech, Gesundheit) ist die Fähigkeit, eine genaue Ereignissequenz zu reproduzieren, unbezahlbar.
MVI macht es einfach, "Time-Travel-Debugging" zu implementieren: alle Zustände aufzeichnen und die Benutzersitzung wiedergeben.
Hybrider Ansatz: Das Beste aus beiden Welten
In der Praxis adoptieren viele Teams einen hybriden Ansatz: MVI für komplexe Screens, vereinfachtes MVVM für einfache Screens. Hier ist ein empfohlenes Muster:
// Base ViewModel with lightweight MVI structure
// Reusable for all screens
abstract class MviViewModel<S, I>(initialState: S) : ViewModel() {
private val _state = MutableStateFlow(initialState)
val state: StateFlow<S> = _state.asStateFlow()
protected val currentState: S get() = _state.value
// Single entry point for intents
abstract fun onIntent(intent: I)
// Helper to update state
protected fun updateState(reducer: S.() -> S) {
_state.update { it.reducer() }
}
}
// Concrete implementation stays simple
class ProfileViewModel(
private val userRepository: UserRepository
) : MviViewModel<ProfileState, ProfileIntent>(ProfileState()) {
override fun onIntent(intent: ProfileIntent) {
when (intent) {
is ProfileIntent.Load -> load(intent.userId)
is ProfileIntent.Refresh -> refresh()
is ProfileIntent.ToggleFavorite -> toggleFavorite()
}
}
private fun load(userId: String) {
viewModelScope.launch {
updateState { copy(isLoading = true) }
val user = userRepository.getUser(userId)
updateState {
copy(user = user, isLoading = false)
}
}
}
private fun refresh() = load(currentState.user?.id ?: return)
private fun toggleFavorite() {
updateState {
copy(user = user?.copy(isFavorite = !user.isFavorite))
}
}
}Dieser Ansatz bietet die Vorteile von MVI (einzelner Zustand, typisierte Intents) ohne übermäßigen Boilerplate.
Empfehlungen für 2026
Hier sind die Empfehlungen für die Wahl zwischen diesen beiden Architekturen je nach Kontext:
Für neue Projekte mit Compose
MVI von Anfang an einsetzen. Compose und MVI teilen dieselbe Philosophie, und die anfängliche Investition amortisiert sich schnell.
Für bestehende View-basierte Projekte
Bei MVVM bleiben, aber schrittweise MVI-Best-Practices übernehmen: einzelner Zustand im ViewModel, typisierte Aktionen mit Sealed Classes.
Für große Teams
Auf einen Ansatz standardisieren und ihn dokumentieren. Konsistenz im Code ist wichtiger als die Wahl des Musters selbst.
Das beste Muster ist das, das das Team versteht und korrekt anwendet. Ein gut implementiertes MVVM schlägt ein schlecht verstandenes MVI.
Fazit
MVVM und MVI sind beide valide Ansätze für die Architektur von Android-Anwendungen. MVVM bietet Einfachheit und Vertrautheit, während MVI Vorhersehbarkeit und einfacheres Debugging bringt.
Entscheidungs-Checkliste
- MVVM wählen, wenn: Junior-Team, einfaches Projekt, kostspielige Migration
- MVI wählen, wenn: natives Compose, komplexer Zustand, kritisches Debugging
- Hybrider Ansatz empfohlen: leichtgewichtiges MVI mit einzelnem Zustand, ohne Over-Engineering
- Höchste Priorität: Konsistenz im gesamten Code
Fang an zu üben!
Teste dein Wissen mit unseren Interview-Simulatoren und technischen Tests.
Unabhängig von der Wahl liegt der Schlüssel darin, die Stärken und Schwächen jedes Ansatzes zu verstehen, um eine fundierte Entscheidung zu treffen. Der beste Code ist jener, den das Team langfristig gelassen warten kann.
Tags
Teilen
Verwandte Artikel

Kotlin Coroutines meistern: Vollständiger Guide 2026
Kotlin Coroutines für die Android-Entwicklung meistern: Suspend-Funktionen, Scopes, Dispatcher und fortgeschrittene Patterns.

React Native: Eine vollständige Mobile App entwickeln (2026)
Umfassende Anleitung zur Entwicklung von iOS- und Android-Apps mit React Native. Vom Setup bis zum Deployment -- alle Grundlagen für den Einstieg.