# 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. - Published: 2026-01-06 - Updated: 2026-04-05 - Author: SharpSkill - Tags: android, mvvm, mvi, architecture, jetpack compose - Reading time: 11 min --- 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. > **Die Einsätze sind hoch** > > 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. ```kotlin // UserProfileViewModel.kt // 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 = _uiState.asStateFlow() // Separate loading state - MVVM allows multiple flows private val _isLoading = MutableStateFlow(false) val isLoading: StateFlow = _isLoading.asStateFlow() // One-shot error messages private val _errorMessage = MutableSharedFlow() val errorMessage: SharedFlow = _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: ```kotlin // ProblematicViewModel.kt // 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>(emptyList()) private val _selectedAddress = MutableStateFlow(null) private val _selectedPayment = MutableStateFlow(null) private val _promoCode = MutableStateFlow(null) private val _isLoading = MutableStateFlow(false) private val _error = MutableStateFlow(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. ```kotlin // UserProfileMviViewModel.kt // 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 = _state.asStateFlow() // Channel for side effects (navigation, snackbar) private val _sideEffect = Channel() val sideEffect: Flow = _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. > **Einfacheres Debugging** > > 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: ```kotlin // UserProfileScreen.kt // 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. ```kotlin // StateComparison.kt // MVVM: potentially fragmented state class MvvmViewModel : ViewModel() { // Multiple sources of truth - manual synchronization needed private val _users = MutableStateFlow>(emptyList()) private val _selectedUser = MutableStateFlow(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 = 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 } ) } } } ``` > **Geisterfehler** > > 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. ```kotlin // TestComparison.kt // 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>() val loadingStates = mutableListOf() 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() 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. ```kotlin // BoilerplateComparison.kt // MVVM: quick start, less code class SimpleViewModel : ViewModel() { private val _name = MutableStateFlow("") val name: StateFlow = _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 = _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 lines ``` Für einen einfachen Screen kann MVI übertrieben erscheinen. Aber diese Struktur zahlt sich aus, wenn der Screen an Komplexität gewinnt. ## 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. ```kotlin // SettingsViewModel.kt // 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. ```kotlin // CheckoutState.kt // Checkout screen with complex state: MVI excels data class CheckoutState( val cartItems: List = emptyList(), val selectedAddress: Address? = null, val selectedPayment: PaymentMethod? = null, val promoCode: PromoCode? = null, val deliveryOptions: List = 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: ```kotlin // MviViewModel.kt // Base ViewModel with lightweight MVI structure // Reusable for all screens abstract class MviViewModel(initialState: S) : ViewModel() { private val _state = MutableStateFlow(initialState) val state: StateFlow = _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()) { 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 eigentliche Kriterium** > > 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 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. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/de/blog/android/mvvm-vs-mvi-architecture