MVI — essensen av mönstret Model-View-Intent i mobila applikationer

Författare: IT Sectr Publicerad: 2026-02-16 Lästid: 10 min

MVI (Model-View-Intent) — ett reaktivt arkitekturmönster baserat på enkelriktat dataflöde och oföränderligt tillstånd. Till skillnad från MVVM, där ViewModel kan ha flera StateFlow, definierar MVI ett enda tillstånd (State), oföränderliga avsikter (Intent) och en ren reducerfunktion (Reducer). MVI garanterar förutsägbarhet av skärmens tillstånd när som helst. Mönstret populariserades i Android-gemenskapen av biblioteken Mosby och Orbit. Mer — i MVIKotlin från Arkadii Ivanov.

Huvudsakliga

  • MVI — Model (tillstånd), View (visning), Intent (användarens avsikt) — reaktiv cykel
  • Unidirectional data flow — data rör sig i en riktning: Intent → Reducer → State → View
  • Immutable State — skärmens tillstånd — ett oföränderligt objekt som återskapas vid varje ändring
  • Reducer — en ren funktion som tar emot aktuellt tillstånd och Intent, returnerar ett nytt tillstånd
  • Side effects — sidoeffekter (nätverk, DB) hanteras separat från Reducer, via Middleware

Vad är MVI: essensen av mönstret Model-View-Intent

MVI (Model-View-Intent) — ett reaktivt arkitekturmönster byggt på principerna från Redux och Cycle.js. Model — skärmens oföränderliga tillstånd, Intent — användarens eller systemets avsikt, View — prenumeration på tillståndet och sändning av Intent. Data rör sig i en cykel: användaren interagerar med View → View skapar en Intent → Intent bearbetas av Reducer → Reducer skapar ett nytt tillstånd → View tar emot det nya tillståndet och ritas om.

Huvudskillnaden mellan MVI och MVVM — en enda källa till sanning (Single Source of Truth). I MVVM kan ViewModel ha flera LiveData/StateFlow (userState, loadingState, errorState), vilket leder till inkonsekvens: loading=true och user=null samtidigt. I MVI finns det exakt en sealed class/interface State som beskriver hela skärmens tillstånd. När som helst är skärmens tillstånd unikt bestämt — det är omöjligt att få loading=true när data redan har laddats. På IT Sectr tillämpar vi MVI för skärmar med komplex logik — beställningsformulär, flerstegsregistreringar, finansiella skärmar — där förutsägbarhet av tillståndet är avgörande.

KomponentRoll i MVIExempel
IntentAnvändarens eller systemets avsiktLoadUser, Refresh, SubmitForm
StateSkärmens oföränderliga tillståndsealed class UserState
ReducerRen funktion: State + Intent → Statefun reduce(state, intent) -> state
MiddlewareHantering av sidoeffekterNätverksbegäran, skrivning till DB

MVI-cykeln består av fem steg: 1) View skickar en Intent (t.ex. LoadUser(42)); 2) Middleware (EffectHandler) utför sidoeffekten — nätverksbegäran; 3) Resultatet återkommer som en ny Intent i systemet; 4) Reducer tar emot aktuellt tillstånd och Intent, skapar ett nytt tillstånd; 5) View tar emot det nya tillståndet och ritas om. Varje steg är förutsägbart och testas isolerat.

MVI i Android: Intent, Reducer, State i Kotlin

MVI på Android implementeras via sealed-klasser för Intent och State, ViewModel med MVI-logik och Jetpack Compose för reaktiv visning. ViewModel tar emot Intent från View, delegerar sidoeffekter till Middleware, kör Reducer och publicerar det nya tillståndet via StateFlow. Jetpack Compose ritar om UI när state ändras — idealiskt för MVI-cykeln.

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

// State — skärmens enda tillstånd
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 — ren funktion
object UserReducer {
    fun reduce(state: UserState, intent: UserIntent): UserState = when (intent) {
        is UserIntent.LoadUser -> UserState.Loading
        is UserIntent.Refresh -> UserState.Loading
    }
}

// ViewModel med 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(/* föregående 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 skickar 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 och sidoeffekter — i MVI kan den rena Reducer inte utföra nätverksbegäranden. Middleware (även EffectHandler eller Bootstrapper) bearbetar Intent, utför sidoeffekten och sänder en ny Intent tillbaka i cykeln. Biblioteken Orbit MVI och MVIKotlin tillhandahåller inbyggt stöd för Middleware med testbara effekter. Utan Middleware degenererar MVI till MVVM med extra Intent- och State-struktur.

MVIKotlin från Arkadii Ivanov — det populäraste MVI-biblioteket för Kotlin Multiplatform. Stöder Android, iOS, web och JVM. Tillhandahåller komponenter: Store (ViewModel), Bootstrapper (initiala effekter), Reducer, Middleware. I oktober 2025 har biblioteket samlat 2,5K stjärnor på GitHub och används i kommersiella projekt, inklusive applikationer från stora ryska banker. På IT Sectr använder vi MVIKotlin för cross-platform KMP-projekt med delad affärslogik.

MVI i iOS: enkelriktat flöde i Swift

MVI på iOS implementeras utan Combine-ViewModel, via Intent → State-cykeln. View skickar Intent via en closure, Reducer — ren funktion, State — struct med oföränderliga fält. SwiftUI ritar om View när State ändras, vilket passar perfekt i MVI-cykeln utan ytterligare @Published-egenskaper. MVI på iOS är särskilt populärt i gemenskapen av SwiftUI-utvecklare som har gått över från Redux (JavaScript).

swift
// State — oföränderlig struktur
struct UserState: Equatable {
    var user: User?
    var isLoading = false
    var errorMessage: String?
}

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

// Reducer — ren 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 — äger tillståndet och hanterar effekter
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 uppdaterar tillståndet
        state = userReducer(state: state, intent: intent)
        // 2. Side effects (om det behövs)
        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) — den populäraste MVI-implementeringen för iOS från Point-Free, byggd på SwiftUI och Combine. TCA tillhandahåller Store, Reducer, Effect och Environment. I oktober 2025 överstiger antalet stjärnor på GitHub 13K — detta är de facto-standarden för MVI på iOS. TCA används i applikationerna Starbucks, Airbnb (delvis) och många indie-projekt. Till skillnad från handskriven MVI löser TCA problem med testning, navigering och sidoeffekter direkt ur lådan.

MVI vs MVVM på iOS — TCA/MVI ger förutsägbarhet av tillståndet, men kräver mer mallkod (Reducer, State, Action). MVVM med @Published är enklare för enkla skärmar. Vi på IT Sectr använder MVVM för 80% av skärmarna och MVI (TCA) för 20% komplexa — finansiella transaktioner, flerstegsformulär, drag-and-play-gränssnitt, där ett tillståndsfel kan kosta användaren pengar.

Jämförelse av MVI med MVVM: när ska man välja MVI

MVI och MVVM löser samma uppgift — organisering av presentationslagret — men med olika metoder för tillståndshantering. MVVM tillåter flera reaktiva källor (LiveData, @Published), vilket kan leda till inkonsekvens. MVI garanterar exakt ett tillstånd varje ögonblick, vilket gör det mer rigoröst och förutsägbart, men ökar kodmängden.

KriteriumMVVMMVI
TillståndFlera LiveData/StateFlowEn sealed class State
DataflödeTvåvägs (View → ViewModel, LiveData → View)Enkelriktat (Intent → Reducer → State → View)
SidoeffekterDirekt i ViewModelVia Middleware/EffectHandler
TestningUnit-tester av ViewModelUnit-tester av Reducer + Middleware
MallkodMinimalReducer + State + Intent + Middleware

När ska man välja MVI — skärmar där tillståndet måste vara strikt deterministiskt: finansiella operationer, varukorg i webbutik, flerstegsformulär med validering vid varje steg. I dessa scenarier är kostnaden för ett tillståndsfel (t.ex. att visa varukorgsbeloppet utan en produkt på grund av en kapplöpning mellan två LiveData) högre än kostnaden för extra kod. I MVVM förlitar du dig på teamets disciplin, i MVI — på arkitekturen.

När MVVM är tillräckligt — 80% av standardskärmarna: användarlista, profil, inställningar, nyhetsflöde. Här är ett enda tillstånd överflödigt och den extra MVI-strukturen saktar ner utvecklingen. På IT Sectr är regeln: om skärmen har 3+ möjliga tillstånd med övergångar (laddning → data → fel → försök igen → laddning → data) — MVI. Om skärmen har 1-2 asynkrona operationer — MVVM.

Bästa praxis för MVI och typiska misstag

Sealed State — bästa praxis för MVI. Tillståndet definieras som en sealed class/interface med varianterna Loading, Success(data), Error(message). Detta garanterar att View inte hamnar i ett inkonsekvent tillstånd — data kan inte visas vid loading=true, eftersom Loading och Success är olika klasser. All data som hör till tillståndet finns inuti sealed-varianten: Success innehåller användaren, Error — felmeddelandet.

Reducer måste förbli en ren funktion — utan API-anrop, DB, SharedPreferences. Den rena funktionen tar emot State och Intent, returnerar State. Sidoeffekter (nätverk, DB, navigering, toasts) hanteras i Middleware eller i Store.dispatch efter anrop av Reducer. Om Reducer är förorenad med sidoeffekter förlorar MVI sin testbarhet och förutsägbarhet — du får MVVM med extra struktur utan fördelar.

Typiska misstag — att deklarera State som en data class med nullable fält istället för en sealed class: data class UserState(val user: User?, val isLoading: Boolean, val error: String?). Detta är motsvarigheten till MVVM, inte MVI — View måste kontrollera fältkombinationer för giltighet. I sealed-metoden är ogiltiga kombinationer (isLoading=true och user!=null) omöjliga på typnivå. Andra misstaget — att placera affärslogik i Intent (Intent.LoadUserBeforeXHours) istället för att skapa enkla kommandon Intent (Intent.LoadUser) och affärslogiken — i Middleware.

Vanliga frågor

Vad är den största skillnaden mellan MVI och MVVM?

MVI använder en enda oföränderlig sealed State-klass och enkelriktat dataflöde via Reducer. MVVM tillåter flera LiveData/StateFlow med tvåvägsbindning. MVI garanterar tillståndskonsistens på typnivå — det är omöjligt att få loading=true och user=null samtidigt. MVVM förlitar sig på utvecklarens disciplin.

Vilka MVI-bibliotek finns för Android?

Huvudsakliga: MVIKotlin (Arkadii Ivanov, 2,5K stjärnor, Kotlin Multiplatform), Orbit MVI (BabyJ, 1,3K stjärnor), Mobius (Spotify, Kotlin/Java). MVIKotlin — mest populärt för Kotlin, Orbit — enklast att lära sig. Alla tre stöder testbar Reducer och Middleware. För Jetpack Compose räcker det att skriva en enkel MVI utan bibliotek via sealed State + Reducer.

Behövs ett separat bibliotek för MVI?

Nej — sealed Intent + sealed State + ViewModel + StateFlow ger en fungerande MVI utan beroenden. Bibliotek (MVIKotlin, Orbit, TCA) lägger till Middleware, testning av sidoeffekter och integration med DI. För enkla projekt är bibliotekets vikt oberättigad. För komplexa projekt med 20+ skärmar lönar sig biblioteket genom strukturerad effekthantering.

Passar MVI för iOS eller är det bara ett Android-mönster?

MVI passar utmärkt för iOS via TCA (The Composable Architecture) — den populäraste arkitekturen i SwiftUI-gemenskapen. TCA är i själva verket MVI + Redux + Combine. På iOS kan MVI implementeras utan TCA via ObservableObject och en ren reducer-funktion. SwiftUI med immutable State passar perfekt i MVI-cykeln.

Hur testar man MVI?

Reducer testas med unit-tester som en ren funktion: ett initialt State ges, en Intent skickas, det slutliga State kontrolleras. Middleware testas med ett mock-repository: man kontrollerar att getUser anropades efter LoadUser. ViewModel-test: skicka en Intent, kontrollera StateFlow. MVI är lättare att testa än MVVM, eftersom Reducer är en ren funktion utan dolda beroenden.

Sammanfattning

  • MVI (Model-View-Intent) — reaktivt mönster med enkelriktat flöde och ett enda tillstånd
  • Sealed State — garanterar konsistens på typnivå, utesluter ogiltiga kombinationer
  • Reducer — ren funktion State + Intent → State, testas utan mock-objekt
  • Middleware — separat lager för sidoeffekter (nätverk, DB, navigering)
  • MVI vs MVVM — MVI är mer rigoröst och förutsägbart, MVVM enklare och snabbare
  • Android — MVIKotlin eller Orbit för komplexa skärmar; MVVM för enkla
  • iOS — TCA (The Composable Architecture) — MVI-standard på SwiftUI

Vi utvecklar en mobil applikation nyckelfärdigt

IT Sectr skapar iOS- och Android-applikationer för startups och företag sedan 2017. Vi ger dig råd och föreslår den bästa lösningen.

Diskutera projektet

Läs också