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-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.
| Komponen | Peran dalam MVI | Contoh |
|---|---|---|
| Intent | Niat pengguna atau sistem | LoadUser, Refresh, SubmitForm |
| State | Keadaan layar yang tidak dapat diubah | sealed class UserState |
| Reducer | Fungsi murni: State + Intent → State | fun reduce(state, intent) -> state |
| Middleware | Pemrosesan efek samping | Permintaan 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 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.
// 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 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).
// 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.
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.
| Kriteria | MVVM | MVI |
|---|---|---|
| Keadaan | Banyak LiveData/StateFlow | Satu sealed class State |
| Aliran data | Dua arah (View → ViewModel, LiveData → View) | Satu arah (Intent → Reducer → State → View) |
| Efek samping | Langsung di ViewModel | Melalui Middleware/EffectHandler |
| Pengujian | Unit test ViewModel | Unit test Reducer + Middleware |
| Kode template | Minimal | Reducer + 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.
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
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.
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.
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.
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.
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
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.
Baca juga