MVI — esensi pola Model-View-Intent dalam aplikasi seluler

Penulis: IT Sectr Diterbitkan: 2026-02-16 Waktu membaca: 10 mnt

MVI (Model-View-Intent) — pola arsitektur reaktif yang didasarkan pada aliran data satu arah dan keadaan yang tidak dapat diubah. Berbeda dengan MVVM, di mana ViewModel dapat memiliki banyak StateFlow, MVI mendefinisikan satu keadaan (State), niat yang tidak dapat diubah (Intent) dan fungsi reducer murni (Reducer). MVI menjamin prediktabilitas keadaan layar kapan saja. Pola ini dipopulerkan di komunitas Android oleh perpustakaan Mosby dan Orbit. Selengkapnya — di MVIKotlin oleh Arkadii Ivanov.

Utama

  • MVI — Model (keadaan), View (tampilan), Intent (niat pengguna) — siklus reaktif
  • Unidirectional data flow — data bergerak dalam satu arah: Intent → Reducer → State → View
  • Immutable State — keadaan layar — objek yang tidak dapat diubah, dibuat ulang pada setiap perubahan
  • Reducer — fungsi murni yang menerima keadaan saat ini dan Intent, mengembalikan keadaan baru
  • Side effects — efek samping (jaringan, DB) diproses terpisah dari Reducer, melalui Middleware

Apa itu MVI: esensi pola Model-View-Intent

MVI (Model-View-Intent) — pola arsitektur reaktif yang dibangun di atas prinsip Redux dan Cycle.js. Model — keadaan layar yang tidak dapat diubah, Intent — niat pengguna atau sistem, View — langganan ke keadaan dan pengiriman Intent. Data bergerak dalam siklus: pengguna berinteraksi dengan View → View membuat Intent → Intent diproses oleh Reducer → Reducer membuat keadaan baru → View menerima keadaan baru dan menggambar ulang.

Perbedaan utama MVI dari MVVM — sumber kebenaran tunggal (Single Source of Truth). Di MVVM, ViewModel dapat memiliki beberapa LiveData/StateFlow (userState, loadingState, errorState), yang menyebabkan ketidakkonsistenan: loading=true dan user=null secara bersamaan. Di MVI ada tepat satu sealed class/interface State yang menggambarkan seluruh keadaan layar. Setiap saat, keadaan layar ditentukan secara unik — tidak mungkin mendapatkan loading=true ketika data sudah dimuat. Di IT Sectr kami menerapkan MVI untuk layar dengan logika kompleks — formulir pemesanan, pendaftaran multi-langkah, layar keuangan — di mana prediktabilitas keadaan sangat penting.

KomponenPeran dalam MVIContoh
IntentNiat pengguna atau sistemLoadUser, Refresh, SubmitForm
StateKeadaan layar yang tidak dapat diubahsealed class UserState
ReducerFungsi murni: State + Intent → Statefun reduce(state, intent) -> state
MiddlewarePemrosesan efek sampingPermintaan jaringan, penulisan ke DB

Siklus MVI terdiri dari lima langkah: 1) View mengirimkan Intent (misalnya, LoadUser(42)); 2) Middleware (EffectHandler) menjalankan efek samping — permintaan jaringan; 3) Hasilnya kembali sebagai Intent baru ke dalam sistem; 4) Reducer menerima keadaan saat ini dan Intent, membuat keadaan baru; 5) View menerima keadaan baru dan menggambar ulang. Setiap langkah dapat diprediksi dan diuji secara terisolasi.

MVI di Android: Intent, Reducer, State di Kotlin

MVI di Android diimplementasikan melalui sealed-class untuk Intent dan State, ViewModel dengan logika MVI dan Jetpack Compose untuk tampilan reaktif. ViewModel menerima Intent dari View, mendelegasikan efek samping ke Middleware, menjalankan Reducer dan mempublikasikan keadaan baru melalui StateFlow. Jetpack Compose menggambar ulang UI saat state berubah — ideal untuk siklus MVI.

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

// State — keadaan layar tunggal
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 — fungsi murni
object UserReducer {
    fun reduce(state: UserState, intent: UserIntent): UserState = when (intent) {
        is UserIntent.LoadUser -> UserState.Loading
        is UserIntent.Refresh -> UserState.Loading
    }
}

// ViewModel dengan 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(/* ID sebelumnya */)
        }
    }

    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 mengirimkan 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 dan efek samping — di MVI, Reducer murni tidak dapat melakukan permintaan jaringan. Middleware (juga EffectHandler atau Bootstrapper) memproses Intent, menjalankan efek samping dan mengeluarkan Intent baru kembali ke siklus. Perpustakaan Orbit MVI dan MVIKotlin menyediakan dukungan bawaan untuk Middleware dengan efek yang dapat diuji. Tanpa Middleware, MVI berubah menjadi MVVM dengan struktur Intent dan State tambahan.

MVIKotlin oleh Arkadii Ivanov — perpustakaan MVI paling populer untuk Kotlin Multiplatform. Mendukung Android, iOS, web dan JVM. Menyediakan komponen: Store (ViewModel), Bootstrapper (efek awal), Reducer, Middleware. Hingga Oktober 2025, perpustakaan ini telah mengumpulkan 2,5K bintang di GitHub dan digunakan dalam proyek komersial, termasuk aplikasi bank-bank besar Rusia. Di IT Sectr kami menggunakan MVIKotlin untuk proyek cross-platform KMP dengan logika bisnis bersama.

MVI di iOS: aliran satu arah di Swift

MVI di iOS diimplementasikan tanpa Combine-ViewModel, melalui siklus Intent → State. View mengirimkan Intent melalui penutupan (closure), Reducer — fungsi murni, State — struct dengan bidang yang tidak dapat diubah. SwiftUI menggambar ulang View saat State berubah, yang sangat cocok dengan siklus MVI tanpa properti @Published tambahan. MVI di iOS sangat populer di komunitas pengembang SwiftUI yang beralih dari Redux (JavaScript).

swift
// State — struktur yang tidak dapat diubah
struct UserState: Equatable {
    var user: User?
    var isLoading = false
    var errorMessage: String?
}

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

// Reducer — fungsi murni
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 — memiliki keadaan dan mengelola efek
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 memperbarui keadaan
        state = userReducer(state: state, intent: intent)
        // 2. Side effects (jika diperlukan)
        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) — implementasi MVI paling populer untuk iOS dari Point-Free, dibangun di atas SwiftUI dan Combine. TCA menyediakan Store, Reducer, Effect dan Environment. Hingga Oktober 2025, jumlah bintang di GitHub melebihi 13K — ini adalah standar de facto untuk MVI di iOS. TCA digunakan di aplikasi Starbucks, Airbnb (sebagian) dan banyak proyek indie. Berbeda dengan MVI yang ditulis manual, TCA menyelesaikan masalah pengujian, navigasi dan efek samping secara siap pakai.

MVI vs MVVM di iOS — TCA/MVI memberikan prediktabilitas keadaan, tetapi membutuhkan lebih banyak kode template (Reducer, State, Action). MVVM dengan @Published lebih sederhana untuk layar sederhana. Kami di IT Sectr menggunakan MVVM untuk 80% layar dan MVI (TCA) untuk 20% yang kompleks — transaksi keuangan, formulir multi-langkah, antarmuka drag-and-drop, di mana kesalahan keadaan dapat merugikan pengguna.

Perbandingan MVI dengan MVVM: kapan memilih MVI

MVI dan MVVM memecahkan tugas yang sama — organisasi lapisan Presentation — tetapi dengan pendekatan berbeda terhadap manajemen keadaan. MVVM memungkinkan beberapa sumber reaktif (LiveData, @Published), yang dapat menyebabkan ketidakkonsistenan. MVI menjamin tepat satu keadaan setiap saat, membuatnya lebih ketat dan dapat diprediksi, tetapi meningkatkan volume kode.

KriteriaMVVMMVI
KeadaanBanyak LiveData/StateFlowSatu sealed class State
Aliran dataDua arah (View → ViewModel, LiveData → View)Satu arah (Intent → Reducer → State → View)
Efek sampingLangsung di ViewModelMelalui Middleware/EffectHandler
PengujianUnit test ViewModelUnit test Reducer + Middleware
Kode templateMinimalReducer + State + Intent + Middleware

Kapan memilih MVI — layar di mana keadaan harus benar-benar deterministik: operasi keuangan, keranjang belanja online, formulir multi-langkah dengan validasi di setiap langkah. Dalam skenario ini, biaya kesalahan keadaan (misalnya, menampilkan jumlah keranjang tanpa satu produk karena perlombaan dua LiveData) lebih tinggi daripada biaya kode tambahan. Di MVVM Anda mengandalkan disiplin tim, di MVI — pada arsitektur.

Kapan MVVM cukup — 80% layar standar: daftar pengguna, profil, pengaturan, umpan berita. Di sini keadaan tunggal berlebihan, dan struktur MVI tambahan akan memperlambat pengembangan. Di IT Sectr aturannya: jika layar memiliki 3+ kemungkinan keadaan dengan transisi (memuat → data → error → ulangi → memuat → data) — MVI. Jika layar memiliki 1-2 operasi asinkron — MVVM.

Praktik terbaik MVI dan kesalahan umum

Sealed State — praktik terbaik MVI. Keadaan didefinisikan sebagai sealed class/interface dengan varian Loading, Success(data), Error(message). Ini menjamin bahwa View tidak akan masuk ke keadaan yang tidak konsisten — data tidak dapat ditampilkan saat loading=true, karena Loading dan Success adalah kelas yang berbeda. Semua data yang berkaitan dengan keadaan berada di dalam varian sealed: Success berisi pengguna, Error — pesan kesalahan.

Reducer harus tetap menjadi fungsi murni — tanpa panggilan API, DB, SharedPreferences. Fungsi murni menerima State dan Intent, mengembalikan State. Efek samping (jaringan, DB, navigasi, toast) diproses di Middleware atau di Store.dispatch setelah memanggil Reducer. Jika Reducer terkontaminasi dengan efek samping, MVI kehilangan kemampuan uji dan prediktabilitasnya — Anda mendapatkan MVVM dengan struktur tambahan tanpa keunggulan.

Kesalahan umum — mendeklarasikan State sebagai data class dengan bidang nullable alih-alih sealed class: data class UserState(val user: User?, val isLoading: Boolean, val error: String?). Ini adalah padanan MVVM, bukan MVI — View harus memeriksa kombinasi bidang untuk validitas. Dalam pendekatan sealed, kombinasi yang tidak valid (isLoading=true dan user!=null) tidak mungkin dilakukan di tingkat tipe. Kesalahan kedua — menempatkan logika bisnis di Intent (Intent.LoadUserBeforeXHours) alih-alih membuat Intent perintah sederhana (Intent.LoadUser), dan logika bisnis — di Middleware.

Pertanyaan yang Sering Diajukan

Apa perbedaan utama antara MVI dan MVVM?

MVI menggunakan satu kelas State sealed yang tidak dapat diubah dan aliran data satu arah melalui Reducer. MVVM memungkinkan banyak LiveData/StateFlow dengan ikatan dua arah. MVI menjamin konsistensi keadaan di tingkat tipe — tidak mungkin mendapatkan loading=true dan user=null secara bersamaan. MVVM mengandalkan disiplin pengembang.

Perpustakaan MVI apa yang ada untuk Android?

Utama: MVIKotlin (Arkadii Ivanov, 2,5K bintang, Kotlin Multiplatform), Orbit MVI (BabyJ, 1,3K bintang), Mobius (Spotify, Kotlin/Java). MVIKotlin — paling populer untuk Kotlin, Orbit — paling mudah dipelajari. Ketiganya mendukung Reducer dan Middleware yang dapat diuji. Untuk Jetpack Compose, cukup menulis MVI sederhana tanpa perpustakaan melalui sealed State + Reducer.

Apakah perpustakaan terpisah diperlukan untuk MVI?

Tidak — sealed Intent + sealed State + ViewModel + StateFlow memberikan MVI yang berfungsi tanpa ketergantungan. Perpustakaan (MVIKotlin, Orbit, TCA) menambahkan Middleware, pengujian efek samping dan integrasi dengan DI. Untuk proyek sederhana, berat perpustakaan tidak dapat dibenarkan. Untuk proyek kompleks dengan 20+ layar, perpustakaan terbayar dengan pemrosesan efek yang terstruktur.

Apakah MVI cocok untuk iOS atau hanya pola Android?

MVI sangat cocok untuk iOS melalui TCA (The Composable Architecture) — arsitektur paling populer di komunitas SwiftUI. TCA pada dasarnya adalah MVI + Redux + Combine. Di iOS, MVI dapat diimplementasikan tanpa TCA melalui ObservableObject dan fungsi reducer murni. SwiftUI dengan State yang tidak dapat diubah sangat cocok dengan siklus MVI.

Bagaimana cara menguji MVI?

Reducer diuji dengan unit test sebagai fungsi murni: diberikan State awal, dikirimkan Intent, diperiksa State akhir. Middleware diuji dengan repository mock: diperiksa bahwa setelah LoadUser, getUser dipanggil. ViewModel test: kirim Intent, periksa StateFlow. MVI lebih mudah diuji daripada MVVM, karena Reducer adalah fungsi murni tanpa ketergantungan tersembunyi.

Kesimpulan

  • MVI (Model-View-Intent) — pola reaktif dengan aliran satu arah dan keadaan tunggal
  • Sealed State — menjamin konsistensi di tingkat tipe, mengecualikan kombinasi yang tidak valid
  • Reducer — fungsi murni State + Intent → State, diuji tanpa objek mock
  • Middleware — lapisan terpisah untuk efek samping (jaringan, DB, navigasi)
  • MVI vs MVVM — MVI lebih ketat dan dapat diprediksi, MVVM lebih sederhana dan lebih cepat
  • Android — MVIKotlin atau Orbit untuk layar kompleks; MVVM untuk yang sederhana
  • iOS — TCA (The Composable Architecture) — standar MVI di SwiftUI

Kami akan mengembangkan aplikasi seluler turnkey

IT Sectr membuat aplikasi iOS dan Android untuk startup dan bisnis sejak 2017. Kami akan memberi saran dan mengusulkan solusi terbaik.

Diskusikan proyek

Baca juga