MVI — kakanyahan ng pattern ng Model-View-Intent sa mga mobile app

May-akda: IT Sectr Nai-publish: 2026-02-16 Oras ng pagbabasa: 10 min

MVI (Model-View-Intent) — isang reaktibong pattern ng arkitektura batay sa unidirectional na daloy ng datos at hindi nababagong estado. Hindi tulad ng MVVM, kung saan ang ViewModel ay maaaring magkaroon ng maraming StateFlow, ang MVI ay tumutukoy sa iisang estado (State), hindi nababagong mga intensyon (Intent) at dalisay na reducer function (Reducer). Ginagarantiyahan ng MVI ang predictability ng estado ng screen sa anumang oras. Ang pattern ay pinasikat sa Android community ng mga library na Mosby at Orbit. Higit pa — sa MVIKotlin mula kay Arkadii Ivanov.

Mga Pangunahing

  • MVI — Model (estado), View (display), Intent (intensyon ng user) — reaktibong cycle
  • Unidirectional data flow — ang datos ay gumagalaw sa isang direksyon: Intent → Reducer → State → View
  • Immutable State — ang estado ng screen — isang hindi nababagong bagay, muling ginagawa sa bawat pagbabago
  • Reducer — dalisay na function na tumatanggap ng kasalukuyang estado at Intent, nagbabalik ng bagong estado
  • Side effects — ang mga side effect (network, DB) ay pinoproseso nang hiwalay mula sa Reducer, sa pamamagitan ng Middleware

Ano ang MVI: kakanyahan ng pattern ng Model-View-Intent

MVI (Model-View-Intent) — isang reaktibong pattern ng arkitektura na binuo sa mga prinsipyo ng Redux at Cycle.js. Model — ang hindi nababagong estado ng screen, Intent — intensyon ng user o system, View — subscription sa estado at pagpapadala ng Intent. Ang datos ay gumagalaw sa isang cycle: ang user ay nakikipag-ugnayan sa View → View ay lumilikha ng Intent → Intent ay pinoproseso ng Reducer → Reducer ay lumilikha ng bagong estado → View ay tumatanggap ng bagong estado at muling gumuhit.

Pangunahing pagkakaiba ng MVI mula sa MVVM — iisang mapagkukunan ng katotohanan (Single Source of Truth). Sa MVVM, ang ViewModel ay maaaring magkaroon ng maraming LiveData/StateFlow (userState, loadingState, errorState), na humahantong sa hindi pagkakapare-pareho: loading=true at user=null nang sabay. Sa MVI mayroong eksaktong isang sealed class/interface State na naglalarawan sa buong estado ng screen. Sa anumang oras, ang estado ng screen ay natatanging natutukoy — imposibleng makakuha ng loading=true kapag ang datos ay na-load na. Sa IT Sectr, inilalapat namin ang MVI para sa mga screen na may kumplikadong lohika — mga form ng order, multi-step na pagpaparehistro, mga screen sa pananalapi — kung saan kritikal ang predictability ng estado.

ComponentPapel sa MVIHalimbawa
IntentIntensyon ng user o systemLoadUser, Refresh, SubmitForm
StateHindi nababagong estado ng screensealed class UserState
ReducerDalisay na function: State + Intent → Statefun reduce(state, intent) -> state
MiddlewarePagproseso ng mga side effectNetwork request, pagsulat sa DB

MVI cycle ay binubuo ng limang hakbang: 1) View ay nagpapadala ng Intent (hal. LoadUser(42)); 2) Middleware (EffectHandler) ay nagsasagawa ng side effect — network request; 3) Ang resulta ay bumalik bilang bagong Intent sa system; 4) Reducer ay tumatanggap ng kasalukuyang estado at Intent, lumilikha ng bagong estado; 5) View ay tumatanggap ng bagong estado at muling gumuhit. Bawat hakbang ay predictable at sinusuri nang hiwalay.

MVI sa Android: Intent, Reducer, State sa Kotlin

MVI sa Android ay ipinapatupad sa pamamagitan ng sealed-class para sa Intent at State, ViewModel na may MVI logic at Jetpack Compose para sa reaktibong display. ViewModel ay tumatanggap ng Intent mula sa View, nagde-delegate ng mga side effect sa Middleware, nagpapatakbo ng Reducer at nag-publish ng bagong estado sa pamamagitan ng StateFlow. Jetpack Compose ay muling gumuhit ng UI kapag nagbago ang state — perpekto para sa MVI cycle.

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

// State — iisang estado ng screen
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 — dalisay na function
object UserReducer {
    fun reduce(state: UserState, intent: UserIntent): UserState = when (intent) {
        is UserIntent.LoadUser -> UserState.Loading
        is UserIntent.Refresh -> UserState.Loading
    }
}

// ViewModel na may 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(/* nakaraang 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 ay nagpapadala ng 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 at mga side effect — sa MVI, ang dalisay na Reducer ay hindi maaaring magsagawa ng mga network request. Middleware (din EffectHandler o Bootstrapper) ay nagpoproseso ng Intent, nagsasagawa ng side effect at naglalabas ng bagong Intent pabalik sa cycle. Ang mga library na Orbit MVI at MVIKotlin ay nagbibigay ng built-in na suporta para sa Middleware na may mga effect na masusuri. Kung walang Middleware, ang MVI ay nagde-degrade sa MVVM na may karagdagang Intent at State na istraktura.

MVIKotlin mula kay Arkadii Ivanov — ang pinakasikat na MVI library para sa Kotlin Multiplatform. Sinusuportahan ang Android, iOS, web at JVM. Nagbibigay ng mga component: Store (ViewModel), Bootstrapper (mga paunang effect), Reducer, Middleware. Noong Oktubre 2025, ang library ay nakaipon ng 2,5K bituin sa GitHub at ginagamit sa mga komersyal na proyekto, kabilang ang mga application ng malalaking bangko sa Russia. Sa IT Sectr, ginagamit namin ang MVIKotlin para sa cross-platform na KMP na mga proyekto na may pinagsamang lohika ng negosyo.

MVI sa iOS: unidirectional na daloy sa Swift

MVI sa iOS ay ipinapatupad nang walang Combine-ViewModel, sa pamamagitan ng Intent → State cycle. View ay nagpapadala ng Intent sa pamamagitan ng closure, Reducer — dalisay na function, State — struct na may hindi nababagong mga field. SwiftUI ay muling gumuhit ng View kapag nagbago ang State, na perpektong umaangkop sa MVI cycle nang walang karagdagang @Published na mga property. MVI sa iOS ay lalong popular sa komunidad ng mga SwiftUI developer na lumipat mula sa Redux (JavaScript).

swift
// State — hindi nababagong istraktura
struct UserState: Equatable {
    var user: User?
    var isLoading = false
    var errorMessage: String?
}

// Intent — enum na may mga intensyon
enum UserIntent {
    case loadUser(id: Int)
    case refresh
    case userLoaded(User)
    case loadFailed(Error)
}

// Reducer — dalisay na function
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 — nagmamay-ari ng estado at namamahala ng mga effect
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 ay nag-a-update ng estado
        state = userReducer(state: state, intent: intent)
        // 2. Side effects (kung kinakailangan)
        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) — ang pinakasikat na implementasyon ng MVI para sa iOS mula sa Point-Free, binuo sa SwiftUI at Combine. TCA ay nagbibigay ng Store, Reducer, Effect at Environment. Noong Oktubre 2025, ang bilang ng mga bituin sa GitHub ay lumampas sa 13K — ito ang de facto na pamantayan para sa MVI sa iOS. TCA ay ginagamit sa mga application ng Starbucks, Airbnb (bahagyang) at maraming indie na proyekto. Hindi tulad ng manu-manong isinulat na MVI, ang TCA ay lumulutas ng mga isyu sa pagsubok, nabigasyon at mga side effect nang handa na.

MVI vs MVVM sa iOS — TCA/MVI ay nagbibigay ng predictability ng estado, ngunit nangangailangan ng mas maraming template code (Reducer, State, Action). MVVM na may @Published ay mas simple para sa mga simpleng screen. Kami sa IT Sectr ay gumagamit ng MVVM para sa 80% ng mga screen at MVI (TCA) para sa 20% na kumplikado — mga transaksyong pinansyal, multi-step na form, drag-and-drop interface, kung saan ang pagkakamali sa estado ay maaaring magdulot ng pera sa user.

Paghahambing ng MVI sa MVVM: kailan pumili ng MVI

MVI at MVVM ay lumulutas ng parehong gawain — organisasyon ng Presentation layer — ngunit may magkaibang approach sa pamamahala ng estado. MVVM ay nagpapahintulot ng maraming reaktibong mapagkukunan (LiveData, @Published), na maaaring humantong sa hindi pagkakapare-pareho. MVI ay ginagarantiyahan ang eksaktong isang estado sa bawat sandali, na ginagawang mas mahigpit at predictable, ngunit pinapataas ang dami ng code.

KraytiryaMVVMMVI
EstadoMaraming LiveData/StateFlowIsang sealed class State
Daloy ng datosBidirectional (View → ViewModel, LiveData → View)Unidirectional (Intent → Reducer → State → View)
Mga side effectDirekta sa ViewModelSa pamamagitan ng Middleware/EffectHandler
PagsubokUnit test ng ViewModelUnit test ng Reducer + Middleware
Template codeMinimalReducer + State + Intent + Middleware

Kailan pumili ng MVI — mga screen kung saan ang estado ay dapat na mahigpit na deterministiko: mga operasyong pinansyal, cart ng online store, multi-step na form na may validation sa bawat hakbang. Sa mga sitwasyong ito, ang halaga ng pagkakamali sa estado (hal. pagpapakita ng halaga ng cart na walang isang produkto dahil sa karera ng dalawang LiveData) ay mas mataas kaysa sa halaga ng karagdagang code. Sa MVVM umaasa ka sa disiplina ng koponan, sa MVI — sa arkitektura.

Kailan sapat ang MVVM — 80% ng mga karaniwang screen: listahan ng user, profile, setting, news feed. Dito ang iisang estado ay kalabisan, at ang karagdagang istraktura ng MVI ay magpapabagal sa pag-unlad. Sa IT Sectr ang patakaran ay: kung ang screen ay may 3+ posibleng estado na may mga transition (load → datos → error → ulitin → load → datos) — MVI. Kung ang screen ay may 1-2 asynchronus na operasyon — MVVM.

Pinakamahusay na kasanayan sa MVI at mga karaniwang pagkakamali

Sealed State — pinakamahusay na kasanayan ng MVI. Ang estado ay tinutukoy bilang sealed class/interface na may mga variant na Loading, Success(data), Error(message). Ito ay ginagarantiyahan na ang View ay hindi mapupunta sa hindi pare-parehong estado — hindi maaaring magpakita ng datos kapag loading=true, dahil ang Loading at Success ay magkaibang klase. Lahat ng datos na nauugnay sa estado ay nasa loob ng sealed variant: Success ay naglalaman ng user, Error — mensahe ng error.

Reducer ay dapat manatiling dalisay na function — walang tawag sa API, DB, SharedPreferences. Ang dalisay na function ay tumatanggap ng State at Intent, nagbabalik ng State. Ang mga side effect (network, DB, nabigasyon, toast) ay pinoproseso sa Middleware o sa Store.dispatch pagkatapos tawagan ang Reducer. Kung ang Reducer ay kontaminado ng mga side effect, ang MVI ay nawawalan ng testability at predictability — makakakuha ka ng MVVM na may karagdagang istraktura nang walang mga pakinabang.

Mga karaniwang pagkakamali — pagdedeklara ng State bilang data class na may nullable field sa halip na sealed class: data class UserState(val user: User?, val isLoading: Boolean, val error: String?). Ito ay katumbas ng MVVM, hindi MVI — View ay dapat suriin ang mga kombinasyon ng field para sa validity. Sa sealed approach, ang mga hindi wastong kombinasyon (isLoading=true at user!=null) ay imposible sa antas ng uri. Pangalawang pagkakamali — paglalagay ng lohika ng negosyo sa Intent (Intent.LoadUserBeforeXHours) sa halip na lumikha ng simpleng command Intent (Intent.LoadUser), at ang lohika ng negosyo — sa Middleware.

Mga Madalas Itanong

Ano ang pangunahing pagkakaiba ng MVI at MVVM?

MVI ay gumagamit ng iisang hindi nababagong sealed State class at unidirectional na daloy ng datos sa pamamagitan ng Reducer. MVVM ay nagpapahintulot ng maraming LiveData/StateFlow na may bidirectional na binding. MVI ay ginagarantiyahan ang consistency ng estado sa antas ng uri — imposibleng makakuha ng loading=true at user=null nang sabay. MVVM ay umaasa sa disiplina ng developer.

Anong mga MVI library ang umiiral para sa Android?

Pangunahing: MVIKotlin (Arkadii Ivanov, 2,5K bituin, Kotlin Multiplatform), Orbit MVI (BabyJ, 1,3K bituin), Mobius (Spotify, Kotlin/Java). MVIKotlin — pinakasikat para sa Kotlin, Orbit — pinakasimpleng matutunan. Lahat ng tatlo ay sumusuporta sa masusuring Reducer at Middleware. Para sa Jetpack Compose, sapat na ang magsulat ng simpleng MVI nang walang library sa pamamagitan ng sealed State + Reducer.

Kailangan ba ng hiwalay na library para sa MVI?

Hindi — sealed Intent + sealed State + ViewModel + StateFlow ay nagbibigay ng gumaganang MVI nang walang dependencies. Ang mga library (MVIKotlin, Orbit, TCA) ay nagdaragdag ng Middleware, pagsubok ng mga side effect at integrasyon sa DI. Para sa mga simpleng proyekto, ang bigat ng library ay hindi makatwiran. Para sa mga kumplikadong proyekto na may 20+ screen, ang library ay nagbabayad sa pamamagitan ng structured na pagproseso ng mga effect.

Angkop ba ang MVI para sa iOS o ito ay pattern lang ng Android?

MVI ay ganap na angkop para sa iOS sa pamamagitan ng TCA (The Composable Architecture) — ang pinakasikat na arkitektura ng SwiftUI community. TCA ay talagang MVI + Redux + Combine. Sa iOS, maaaring ipatupad ang MVI nang walang TCA sa pamamagitan ng ObservableObject at dalisay na reducer function. SwiftUI na may immutable State ay perpektumaangkop sa MVI cycle.

Paano subukan ang MVI?

Reducer ay sinusuri gamit ang unit test bilang dalisay na function: bibigyan ng paunang State, magpapadala ng Intent, susuriin ang huling State. Middleware ay sinusuri gamit ang mock-repository: susuriin kung pagkatapos ng LoadUser ay tinawag ang getUser. ViewModel test: magpadala ng Intent, suriin ang StateFlow. MVI ay mas madaling subukan kaysa MVVM, dahil ang Reducer ay dalisay na function na walang nakatagong dependencies.

Buod

  • MVI (Model-View-Intent) — reaktibong pattern na may unidirectional na daloy at iisang estado
  • Sealed State — ginagarantiyahan ang consistency sa antas ng uri, hindi kasama ang mga hindi wastong kombinasyon
  • Reducer — dalisay na function State + Intent → State, sinusuri nang walang mock na bagay
  • Middleware — hiwalay na layer para sa mga side effect (network, DB, nabigasyon)
  • MVI vs MVVM — MVI ay mas mahigpit at predictable, MVVM ay mas simple at mas mabilis
  • Android — MVIKotlin o Orbit para sa mga kumplikadong screen; MVVM para sa mga simple
  • iOS — TCA (The Composable Architecture) — pamantayan ng MVI sa SwiftUI

Gagawa kami ng mobile application na turnkey

Gumagawa ang IT Sectr ng mga iOS at Android application para sa mga startup at negosyo mula noong 2017. Magpapayo kami sa iyo at magmumungkahi ng pinakamahusay na solusyon.

Pag-usapan ang proyekto

Basahin din