MVI — Das Model-View-Intent Pattern in mobilen Apps verstehen

Autor: IT Sectr Veröffentlicht: 2026-02-16 Lesezeit: 10 Min.

MVI (Model-View-Intent) ist ein reaktives Architekturmuster, das auf unidirektionalem Datenfluss und unveränderlichem Zustand basiert. Im Gegensatz zu MVVM, wo eine ViewModel mehrere StateFlows haben kann, definiert MVI einen einzigen Zustand (State), unveränderliche Absichten (Intent) und eine reine Reducer-Funktion (Reducer). MVI garantiert die Vorhersagbarkeit des Bildschirmzustands zu jedem Zeitpunkt. Das Muster wurde in der Android-Community durch die Bibliotheken Mosby und Orbit populär gemacht. Mehr Informationen in MVIKotlin von Arkadii Ivanov.

Wichtige Erkenntnisse

  • MVI — Model (Zustand), View (Anzeige), Intent (Benutzerabsicht) — ein reaktiver Zyklus
  • Unidirectional data flow — Daten fließen in eine Richtung: Intent → Reducer → State → View
  • Immutable State — der Bildschirmzustand ist ein unveränderliches Objekt, das bei jeder Änderung neu erstellt wird
  • Reducer — eine reine Funktion, die den aktuellen Zustand und einen Intent nimmt und einen neuen Zustand zurückgibt
  • Side effects — Nebeneffekte (Netzwerk, DB) werden getrennt vom Reducer über Middleware behandelt

Was ist MVI: Das Wesen des Model-View-Intent Patterns

MVI (Model-View-Intent) ist ein reaktives Architekturmuster, das auf den Prinzipien von Redux und Cycle.js aufbaut. Model ist der unveränderliche Bildschirmzustand, Intent ist eine Benutzer- oder Systemabsicht, View abonniert den Zustand und sendet Intents. Daten fließen in einem Zyklus: der Benutzer interagiert mit der View → die View erstellt einen Intent → der Intent wird vom Reducer verarbeitet → der Reducer erstellt einen neuen Zustand → die View erhält den neuen Zustand und rendert neu.

Der Hauptunterschied zwischen MVI und MVVM ist die einzige Quelle der Wahrheit (Single Source of Truth). In MVVM kann eine ViewModel mehrere LiveData/StateFlow (userState, loadingState, errorState) haben, was zu Inkonsistenz führt: loading=true und user=null gleichzeitig. In MVI gibt es genau eine sealed class/interface State, die den gesamten Bildschirmzustand beschreibt. Zu jedem Zeitpunkt ist der Bildschirmzustand eindeutig bestimmt — es ist unmöglich, loading=true zu haben, wenn Daten bereits geladen sind. Bei IT Sectr verwenden wir MVI für Bildschirme mit komplexer Logik — Bestellformulare, mehrstufige Registrierungen, Finanzbildschirme — wo Zustandsvorhersagbarkeit kritisch ist.

KomponenteRolle in MVIBeispiel
IntentBenutzer- oder SystemabsichtLoadUser, Refresh, SubmitForm
StateUnveränderlicher Bildschirmzustandsealed class UserState
ReducerReine Funktion: State + Intent → Statefun reduce(state, intent) -> state
MiddlewareNebeneffektbehandlungNetzwerkanfrage, DB-Schreibzugriff

Der MVI-Zyklus besteht aus fünf Schritten: 1) Die View sendet einen Intent (z.B. LoadUser(42)); 2) Middleware (EffectHandler) führt einen Nebeneffekt aus — eine Netzwerkanfrage; 3) Das Ergebnis wird als neuer Intent in das System zurückgegeben; 4) Reducer nimmt den aktuellen Zustand und Intent und erstellt einen neuen Zustand; 5) Die View erhält den neuen Zustand und rendert neu. Jeder Schritt ist vorhersagbar und isoliert testbar.

MVI in Android: Intent, Reducer, State in Kotlin

MVI auf Android wird mit sealed-Klassen für Intent und State, einer ViewModel mit MVI-Logik und Jetpack Compose für reaktives Rendering implementiert. Die ViewModel empfängt Intent von der View, delegiert Nebeneffekte an Middleware, führt den Reducer aus und veröffentlicht den neuen Zustand über StateFlow. Jetpack Compose rendert die UI neu, wenn sich der Zustand ändert — ideal für den MVI-Zyklus.

kotlin
// Intent — Benutzerabsichten
sealed interface UserIntent {
    data class LoadUser(val userId: Int) : UserIntent
    data object Refresh : UserIntent
}

// State — einheitlicher Bildschirmzustand
sealed interface UserState {
    data object Idle : UserState
    data object Loading : UserState
    data class Success(val user: User) : UserState
    data class Error(val message: String) : UserState
}

// Reducer — reine Funktion
object UserReducer {
    fun reduce(state: UserState, intent: UserIntent): UserState = when (intent) {
        is UserIntent.LoadUser -> UserState.Loading
        is UserIntent.Refresh -> UserState.Loading
    }
}

// ViewModel mit MVI
class UserViewModel(
    private val repository: UserRepository
) : ViewModel() {

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

    fun process(intent: UserIntent) {
        val newState = UserReducer.reduce(_state.value, intent)
        _state.value = newState
        when (intent) {
            is UserIntent.LoadUser -> loadUser(intent.userId)
            is UserIntent.Refresh -> loadUser(/* vorherige ID */)
        }
    }

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

// View sendet Intent
fun UserScreen(viewModel: UserViewModel) {
    val state by viewModel.state.collectAsState()
    LaunchedEffect(Unit) { viewModel.process(UserIntent.LoadUser(42)) }
    when (state) {
        is UserState.Loading -> CircularProgressIndicator()
        is UserState.Success -> UserCard((state as UserState.Success).user)
        is UserState.Error -> ErrorView((state as UserState.Error).message)
        is UserState.Idle -> Text("Press to load")
    }
}

Middleware und Nebeneffekte — in MVI kann ein reiner Reducer keine Netzwerkanfragen durchführen. Middleware (auch EffectHandler oder Bootstrapper) verarbeitet den Intent, führt den Nebeneffekt aus und sendet einen neuen Intent zurück in den Zyklus. Die Bibliotheken Orbit MVI und MVIKotlin bieten integrierte Middleware-Unterstützung mit testbaren Effekten. Ohne Middleware degeneriert MVI zu MVVM mit zusätzlicher Intent- und State-Struktur.

MVIKotlin von Arkadii Ivanov ist die beliebteste MVI-Bibliothek für Kotlin Multiplatform. Sie unterstützt Android, iOS, Web und JVM. Sie bietet Komponenten: Store (ViewModel), Bootstrapper (initiale Effekte), Reducer, Middleware. Stand Oktober 2025 hat die Bibliothek 2,5K Sterne auf GitHub und wird in kommerziellen Projekten eingesetzt, darunter Anwendungen großer russischer Banken. Bei IT Sectr verwenden wir MVIKotlin für plattformübergreifende KMP-Projekte mit gemeinsamer Geschäftslogik.

MVI in iOS: unidirektionaler Fluss in Swift

MVI auf iOS wird ohne Combine-ViewModel, über den Intent → State-Zyklus implementiert. Die View sendet einen Intent über einen Closure, der Reducer ist eine reine Funktion, und State ist eine Struktur mit unveränderlichen Feldern. SwiftUI rendert die View neu, wenn sich State ändert, was perfekt in den MVI-Zyklus ohne zusätzliche @Published-Eigenschaften passt. MVI auf iOS ist besonders beliebt bei SwiftUI-Entwicklern, die von Redux (JavaScript) umgestiegen sind.

swift
// State — unveränderliche Struktur
struct UserState: Equatable {
    var user: User?
    var isLoading = false
    var errorMessage: String?
}

// Intent — enum mit Absichten
enum UserIntent {
    case loadUser(id: Int)
    case refresh
    case userLoaded(User)
    case loadFailed(Error)
}

// Reducer — reine Funktion
func userReducer(state: UserState, intent: UserIntent) -> UserState {
    var newState = state
    switch intent {
    case .loadUser, .refresh:
        newState.isLoading = true
        newState.errorMessage = nil
    case .userLoaded(let user):
        newState.isLoading = false
        newState.user = user
    case .loadFailed(let error):
        newState.isLoading = false
        newState.errorMessage = error.localizedDescription
    }
    return newState
}

// Store — besitzt Zustand und verwaltet Effekte
final class UserStore: ObservableObject {
    @Published private(set) var state = UserState()
    private let service: UserService

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

    func dispatch(_ intent: UserIntent) {
        // 1. Reducer aktualisiert Zustand
        state = userReducer(state: state, intent: intent)
        // 2. Side effects (falls nötig)
        switch intent {
        case .loadUser(let id), .refresh:
            service.fetchUser(id: id) { [weak self] result in
                switch result {
                case .success(let user):
                    self?.dispatch(.userLoaded(user))
                case .failure(let error):
                    self?.dispatch(.loadFailed(error))
                }
            }
        default: break
        }
    }
}

TCA (The Composable Architecture) ist die beliebteste MVI-Implementierung für iOS von Point-Free, aufgebaut auf SwiftUI und Combine. TCA bietet Store, Reducer, Effect und Environment. Stand Oktober 2025 übersteigen ihre GitHub-Sterne 13K — es ist der De-facto-Standard für MVI auf iOS. TCA wird in Starbucks-Apps, Airbnb (teilweise) und vielen Indie-Projekten verwendet. Im Gegensatz zu benutzerdefiniertem MVI adressiert TCA Tests, Navigation und Nebeneffekte sofort.

MVI vs MVVM auf iOS — TCA/MVI bietet Zustandsvorhersagbarkeit, erfordert aber mehr Boilerplate-Code (Reducer, State, Action). MVVM mit @Published ist für einfache Bildschirme einfacher. Bei IT Sectr verwenden wir MVVM für 80% der Bildschirme und MVI (TCA) für 20% komplexe — Finanztransaktionen, mehrstufige Formulare, Drag-and-Drop-Schnittstellen, wo ein Zustandsfehler den Benutzer Geld kosten könnte.

MVI vs MVVM: Wann MVI wählen

MVI und MVVM lösen dasselbe Problem — die Organisation der Präsentationsschicht — aber mit unterschiedlichen Ansätzen zur Zustandsverwaltung. MVVM erlaubt mehrere reaktive Quellen (LiveData, @Published), was zu Inkonsistenz führen kann. MVI garantiert genau einen Zustand zu jedem Zeitpunkt, was es strenger und vorhersagbarer macht, aber die Code-Menge erhöht.

KriteriumMVVMMVI
ZustandMehrere LiveData/StateFlowEinzelne sealed class State
DatenflussBidirektional (View → ViewModel, LiveData → View)Unidirektional (Intent → Reducer → State → View)
NebeneffekteDirekt in ViewModelÜber Middleware/EffectHandler
TestsUnit-Tests für ViewModelUnit-Tests für Reducer + Middleware
Boilerplate-CodeMinimalReducer + State + Intent + Middleware

Wann MVI wählen — Bildschirme, bei denen der Zustand streng deterministisch sein muss: Finanzoperationen, Einkaufswagen, mehrstufige Formulare mit Validierung in jedem Schritt. In diesen Szenarien überwiegen die Kosten eines Zustandsfehlers (z.B. Anzeige des Warenkorbgesamtbetrags ohne einen Artikel aufgrund einer Racebedingung zwischen zwei LiveData) die Kosten für zusätzlichen Code. Bei MVVM verlassen Sie sich auf Teamdisziplin, bei MVI auf die Architektur.

Wann MVVM ausreicht — 80% der Standardbildschirme: Benutzerliste, Profil, Einstellungen, Nachrichtenfeed. Hier ist ein einzelner Zustand übertrieben und die zusätzliche MVI-Struktur verlangsamt die Entwicklung. Bei IT Sectr gilt die Regel: Wenn ein Bildschirm 3+ mögliche Zustände mit Übergängen hat (Laden → Daten → Fehler → Wiederholen → Laden → Daten) — verwenden Sie MVI. Wenn ein Bildschirm 1-2 asynchrone Operationen hat — verwenden Sie MVVM.

MVI Best Practices und häufige Fehler

Sealed State — Best Practice in MVI. Der Zustand wird als sealed class/interface mit Varianten definiert: Loading, Success(data), Error(message). Dies garantiert, dass die View nicht in einen inkonsistenten Zustand gerät — Daten können nicht angezeigt werden, wenn loading=true ist, da Loading und Success verschiedene Klassen sind. Alle zustandsbezogenen Daten befinden sich innerhalb der sealed-Variante: Success enthält den Benutzer, Error enthält die Fehlermeldung.

Der Reducer muss eine reine Funktion bleiben — ohne API-, DB- oder SharedPreferences-Aufrufe. Eine reine Funktion nimmt State und Intent und gibt State zurück. Nebeneffekte (Netzwerk, DB, Navigation, Toasts) werden in Middleware oder in Store.dispatch nach dem Aufruf des Reducers behandelt. Wenn der Reducer mit Nebeneffekten verunreinigt wird, verliert MVI Testbarkeit und Vorhersagbarkeit — Sie erhalten MVVM mit zusätzlicher Struktur ohne Vorteile.

Häufige Fehler — Deklarieren von State als data class mit nullable-Feldern anstelle einer sealed class: data class UserState(val user: User?, val isLoading: Boolean, val error: String?). Dies ist äquivalent zu MVVM, nicht MVI — die View muss Feldkombinationen auf Gültigkeit prüfen. Im sealed-Ansatz sind ungültige Kombinationen (isLoading=true und user!=null) auf Typebene unmöglich. Der zweite Fehler ist das Platzieren von Geschäftslogik in Intent (Intent.LoadUserBeforeXHours) anstatt einfache Befehls-Intents (Intent.LoadUser) zu erstellen und die Geschäftslogik in Middleware zu platzieren.

Häufig gestellte Fragen

Was ist der Hauptunterschied zwischen MVI und MVVM?

MVI verwendet eine einzige unveränderliche sealed State-Klasse und unidirektionalen Datenfluss durch einen Reducer. MVVM erlaubt mehrere LiveData/StateFlow mit bidirektionaler Bindung. MVI garantiert Zustandskonsistenz auf Typebene — es ist unmöglich, loading=true und user=null gleichzeitig zu haben. MVVM verlässt sich auf die Disziplin des Entwicklers.

Welche MVI-Bibliotheken gibt es für Android?

Hauptsächlich: MVIKotlin (Arkadii Ivanov, 2,5K Sterne, Kotlin Multiplatform), Orbit MVI (BabyJ, 1,3K Sterne), Mobius (Spotify, Kotlin/Java). MVIKotlin ist die beliebteste für Kotlin, Orbit ist am einfachsten zu lernen. Alle drei unterstützen testbare Reducer und Middleware. Für Jetpack Compose können Sie einfaches MVI ohne Bibliothek mit sealed State + Reducer schreiben.

Ist eine separate Bibliothek für MVI erforderlich?

Nein — sealed Intent + sealed State + ViewModel + StateFlow ergeben funktionierendes MVI ohne Abhängigkeiten. Bibliotheken (MVIKotlin, Orbit, TCA) fügen Middleware, Nebeneffekttests und DI-Integration hinzu. Für einfache Projekte ist das Bibliotheksgewicht ungerechtfertigt. Für komplexe Projekte mit 20+ Bildschirmen zahlt sich die Bibliothek durch strukturierte Effektbehandlung aus.

Ist MVI für iOS geeignet oder ist es ein reines Android-Muster?

MVI funktioniert hervorragend für iOS über TCA (The Composable Architecture) — die beliebteste Architektur der SwiftUI-Community. TCA ist im Wesentlichen MVI + Redux + Combine. Auf iOS können Sie MVI ohne TCA mit ObservableObject und einer reinen Reducer-Funktion implementieren. SwiftUI mit unveränderlichem State passt perfekt in den MVI-Zyklus.

Wie testet man MVI?

Der Reducer wird mit Unit-Tests als reine Funktion getestet: initialen State setzen, Intent senden, resultierenden State prüfen. Middleware wird mit einem Mock-Repository getestet: überprüfen, ob getUser nach LoadUser aufgerufen wurde. ViewModel-Test: Intent senden, StateFlow prüfen. MVI ist einfacher zu testen als MVVM, weil der Reducer eine reine Funktion ohne versteckte Abhängigkeiten ist.

Zusammenfassung

  • MVI (Model-View-Intent) — ein reaktives Muster mit unidirektionalem Fluss und einem einzigen Zustand
  • Sealed State — garantiert Konsistenz auf Typebene, eliminiert ungültige Kombinationen
  • Reducer — reine Funktion State + Intent → State, ohne Mock-Objekte testbar
  • Middleware — eine separate Schicht für Nebeneffekte (Netzwerk, DB, Navigation)
  • MVI vs MVVM — MVI ist strenger und vorhersagbarer, MVVM ist einfacher und schneller
  • Android — MVIKotlin oder Orbit für komplexe Bildschirme; MVVM für einfache
  • iOS — TCA (The Composable Architecture) ist der MVI-Standard auf SwiftUI

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.

Projekt besprechen

Lesen Sie auch