MVVM vs MVI di Android: Arsitektur Mana yang Dipilih di 2026?
Perbandingan mendalam antara MVVM dan MVI di Android: kelebihan, keterbatasan, kasus penggunaan, dan panduan praktis memilih arsitektur yang tepat di 2026.

Memilih arsitektur yang tepat adalah keputusan krusial yang berdampak pada kemampuan pemeliharaan, pengujian, dan skalabilitas aplikasi Android. Di tahun 2026, dua pola mendominasi ekosistem: MVVM, standar industri, dan MVI, pendekatan reaktif yang semakin populer bersama Jetpack Compose.
Pilihan arsitektur yang buruk sangat mahal: utang teknis, bug yang sulit direproduksi, dan refaktorisasi yang menyakitkan. Memahami kekuatan dan kelemahan setiap pendekatan menghemat banyak masalah di kemudian hari.
Memahami MVVM: Standar yang Sudah Mapan
MVVM (Model-View-ViewModel) adalah arsitektur yang direkomendasikan Google sejak diperkenalkannya Jetpack. Arsitektur ini memisahkan tanggung jawab ke dalam tiga lapisan yang jelas, membuat kode lebih terorganisir dan mudah diuji.
Prinsip Inti MVVM
Pola MVVM mengandalkan pemisahan yang jelas: Model mengelola data dan logika bisnis, View menampilkan antarmuka, dan ViewModel menjembatani keduanya dengan menyediakan state yang dapat diamati.
Berikut implementasi layar profil pengguna dengan MVVM. Contoh pertama ini menunjukkan struktur dasar dengan ViewModel yang mengekspos state yang dapat diamati dan metode untuk interaksi pengguna.
// Classic MVVM ViewModel for a user profile screen
// It exposes observable state and methods for actions
class UserProfileViewModel(
private val userRepository: UserRepository,
private val analyticsTracker: AnalyticsTracker
) : ViewModel() {
// Observable state with StateFlow - the View observes these changes
private val _uiState = MutableStateFlow(UserProfileState())
val uiState: StateFlow<UserProfileState> = _uiState.asStateFlow()
// Separate loading state - MVVM allows multiple flows
private val _isLoading = MutableStateFlow(false)
val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()
// One-shot error messages
private val _errorMessage = MutableSharedFlow<String>()
val errorMessage: SharedFlow<String> = _errorMessage.asSharedFlow()
// Initial profile loading
fun loadProfile(userId: String) {
viewModelScope.launch {
_isLoading.value = true
try {
// Repository call to fetch data
val user = userRepository.getUser(userId)
// Update state with new data
_uiState.update { currentState ->
currentState.copy(
user = user,
isEditing = false
)
}
// Analytics tracking
analyticsTracker.trackProfileViewed(userId)
} catch (e: Exception) {
// Emit one-shot error message
_errorMessage.emit("Unable to load profile")
} finally {
_isLoading.value = false
}
}
}
// Enable edit mode
fun enableEditMode() {
_uiState.update { it.copy(isEditing = true) }
}
// Save profile changes
fun saveProfile(name: String, bio: String) {
viewModelScope.launch {
_isLoading.value = true
try {
val updatedUser = userRepository.updateUser(
_uiState.value.user?.id ?: return@launch,
name = name,
bio = bio
)
_uiState.update {
it.copy(user = updatedUser, isEditing = false)
}
} catch (e: Exception) {
_errorMessage.emit("Failed to save profile")
} finally {
_isLoading.value = false
}
}
}
}
// Data class representing the screen state
data class UserProfileState(
val user: User? = null,
val isEditing: Boolean = false
)ViewModel ini mengilustrasikan pendekatan MVVM yang khas: beberapa flow yang dapat diamati (state utama, loading, error) dan metode publik untuk setiap aksi pengguna.
Keunggulan MVVM
MVVM memiliki beberapa kekuatan yang menjelaskan adopsinya yang masif:
- Keakraban: Sebagian besar pengembang Android mengenal pola ini
- Fleksibilitas: State dapat distrukturkan sesuai keinginan
- Ekosistem: Integrasi sempurna dengan Jetpack (LiveData, StateFlow, Hilt)
- Kesederhanaan: Kurva pembelajaran yang landai untuk pemula
MVVM sangat cocok untuk tim campuran dengan pengembang berbagai tingkat keahlian. Kesederhanaan konseptualnya memudahkan proses onboarding.
Keterbatasan MVVM
Namun MVVM menunjukkan keterbatasannya seiring pertumbuhan aplikasi. Masalah utama adalah pengelolaan state yang terdistribusi. Contoh berikut mengilustrasikan masalah umum ini:
// Example MVVM ViewModel with fragmented state
// This pattern becomes problematic as the screen grows in complexity
class CheckoutViewModel : ViewModel() {
// Problem: state scattered across multiple flows
private val _cart = MutableStateFlow<List<CartItem>>(emptyList())
private val _selectedAddress = MutableStateFlow<Address?>(null)
private val _selectedPayment = MutableStateFlow<PaymentMethod?>(null)
private val _promoCode = MutableStateFlow<String?>(null)
private val _isLoading = MutableStateFlow(false)
private val _error = MutableStateFlow<String?>(null)
// Each modification can create temporary inconsistent states
fun applyPromoCode(code: String) {
viewModelScope.launch {
_isLoading.value = true
_error.value = null // Reset error
try {
val discount = promoRepository.validate(code)
_promoCode.value = code
// Cart state also needs updating...
// but there's a delay between the two updates
recalculateCart()
} catch (e: Exception) {
_error.value = e.message
_promoCode.value = null
} finally {
_isLoading.value = false
}
}
}
// Hard to guarantee consistency across all these states
private fun recalculateCart() {
// Complex logic depending on multiple states...
}
}Contoh ini menunjukkan bagaimana state bisa terfragmentasi di MVVM, mempersulit pelacakan transisi dan reproduksi bug.
Memahami MVI: Pendekatan Satu Arah
MVI (Model-View-Intent) mengadopsi filosofi yang berbeda: aliran data satu arah dan satu state yang tidak dapat diubah. Pendekatan yang terinspirasi dari Redux ini menghilangkan masalah state yang tidak konsisten.
Prinsip Inti MVI
Dalam MVI, segalanya mengikuti siklus yang jelas: pengguna mengirimkan Intent (aksi), Reducer mengubah state saat ini menjadi state baru, dan View menampilkan satu state tersebut. Ini dapat diprediksi, diuji, dan di-debug.
Berikut implementasi layar profil yang sama dengan MVI. Perhatikan bagaimana state dipusatkan dan aksi diketik secara eksplisit.
// MVI ViewModel for the same user profile screen
// Note the structure: Intent → Reducer → Single State
class UserProfileMviViewModel(
private val userRepository: UserRepository,
private val analyticsTracker: AnalyticsTracker
) : ViewModel() {
// Single, immutable state - the absolute source of truth
private val _state = MutableStateFlow(UserProfileState())
val state: StateFlow<UserProfileState> = _state.asStateFlow()
// Channel for side effects (navigation, snackbar)
private val _sideEffect = Channel<UserProfileSideEffect>()
val sideEffect: Flow<UserProfileSideEffect> = _sideEffect.receiveAsFlow()
// Single entry point for all user actions
fun onIntent(intent: UserProfileIntent) {
when (intent) {
is UserProfileIntent.LoadProfile -> loadProfile(intent.userId)
is UserProfileIntent.EnableEditMode -> enableEditMode()
is UserProfileIntent.SaveProfile -> saveProfile(intent.name, intent.bio)
is UserProfileIntent.CancelEdit -> cancelEdit()
}
}
private fun loadProfile(userId: String) {
viewModelScope.launch {
// Transition to loading state
_state.update { it.copy(isLoading = true, error = null) }
try {
val user = userRepository.getUser(userId)
// Single atomic state update
_state.update {
it.copy(
user = user,
isLoading = false,
error = null
)
}
analyticsTracker.trackProfileViewed(userId)
} catch (e: Exception) {
// Error state is part of the main state
_state.update {
it.copy(
isLoading = false,
error = "Unable to load profile"
)
}
}
}
}
private fun enableEditMode() {
// Simple, predictable update
_state.update { it.copy(isEditing = true) }
}
private fun saveProfile(name: String, bio: String) {
viewModelScope.launch {
val currentUser = _state.value.user ?: return@launch
_state.update { it.copy(isLoading = true) }
try {
val updatedUser = userRepository.updateUser(
currentUser.id,
name = name,
bio = bio
)
_state.update {
it.copy(
user = updatedUser,
isEditing = false,
isLoading = false
)
}
// Side effect to notify the user
_sideEffect.send(UserProfileSideEffect.ShowSuccess("Profile updated"))
} catch (e: Exception) {
_state.update {
it.copy(isLoading = false, error = "Failed to save profile")
}
}
}
}
private fun cancelEdit() {
_state.update { it.copy(isEditing = false) }
}
}
// All possible actions, explicitly typed
sealed class UserProfileIntent {
data class LoadProfile(val userId: String) : UserProfileIntent()
object EnableEditMode : UserProfileIntent()
data class SaveProfile(val name: String, val bio: String) : UserProfileIntent()
object CancelEdit : UserProfileIntent()
}
// Single, complete screen state
data class UserProfileState(
val user: User? = null,
val isLoading: Boolean = false,
val isEditing: Boolean = false,
val error: String? = null
)
// One-shot side effects
sealed class UserProfileSideEffect {
data class ShowSuccess(val message: String) : UserProfileSideEffect()
data class NavigateTo(val destination: String) : UserProfileSideEffect()
}Perbedaannya jelas: satu aliran state, aksi yang eksplisit, dan pemisahan bersih antara state persisten dan efek sekali pakai.
Dengan MVI, setiap Intent dan setiap transisi state dapat dicatat. Mereproduksi bug menjadi sepele: cukup putar ulang urutan Intent.
MVI dengan Jetpack Compose
MVI bersinar secara khusus dengan Jetpack Compose, karena keduanya berbagi filosofi yang sama: state yang tidak dapat diubah dan antarmuka deklaratif. Berikut cara menghubungkan ViewModel ke layar Compose:
// Compose screen consuming MVI state
// The connection between ViewModel and UI is elegant and reactive
@Composable
fun UserProfileScreen(
viewModel: UserProfileMviViewModel = hiltViewModel(),
onNavigateBack: () -> Unit
) {
// Collect the single state
val state by viewModel.state.collectAsStateWithLifecycle()
// Handle side effects
LaunchedEffect(Unit) {
viewModel.sideEffect.collect { effect ->
when (effect) {
is UserProfileSideEffect.ShowSuccess -> {
// Show snackbar
}
is UserProfileSideEffect.NavigateTo -> {
// Navigate
}
}
}
}
// Purely declarative UI based on state
UserProfileContent(
state = state,
onIntent = viewModel::onIntent
)
}
@Composable
private fun UserProfileContent(
state: UserProfileState,
onIntent: (UserProfileIntent) -> Unit
) {
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
// Conditional rendering based on the single state
when {
state.isLoading -> {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.CenterHorizontally)
)
}
state.error != null -> {
ErrorMessage(
message = state.error,
onRetry = {
state.user?.id?.let {
onIntent(UserProfileIntent.LoadProfile(it))
}
}
)
}
state.user != null -> {
ProfileCard(
user = state.user,
isEditing = state.isEditing,
onEditClick = { onIntent(UserProfileIntent.EnableEditMode) },
onSaveClick = { name, bio ->
onIntent(UserProfileIntent.SaveProfile(name, bio))
},
onCancelClick = { onIntent(UserProfileIntent.CancelEdit) }
)
}
}
}
}Antarmuka menjadi fungsi murni dari state: dapat diprediksi, diuji, dan tanpa efek samping tersembunyi.
Perbandingan Mendetail
Setelah melihat kedua pola dalam praktik, keduanya dapat dibandingkan berdasarkan kriteria yang benar-benar penting di lingkungan produksi.
Manajemen State
Perbedaan mendasar terletak pada manajemen state. Perbedaan ini berdampak langsung pada kemampuan pemeliharaan jangka panjang.
// MVVM: potentially fragmented state
class MvvmViewModel : ViewModel() {
// Multiple sources of truth - manual synchronization needed
private val _users = MutableStateFlow<List<User>>(emptyList())
private val _selectedUser = MutableStateFlow<User?>(null)
private val _isLoading = MutableStateFlow(false)
private val _searchQuery = MutableStateFlow("")
// What happens if _selectedUser points to a user
// that's no longer in _users after a refresh?
// → Inconsistent state that's hard to detect
}
// MVI: consistent state by construction
class MviViewModel : ViewModel() {
// Single source of truth - inconsistencies are impossible
private val _state = MutableStateFlow(UsersState())
data class UsersState(
val users: List<User> = emptyList(),
val selectedUser: User? = null, // Always consistent with users
val isLoading: Boolean = false,
val searchQuery: String = ""
)
// Each update automatically maintains invariants
private fun selectUser(userId: String) {
_state.update { currentState ->
currentState.copy(
selectedUser = currentState.users.find { it.id == userId }
)
}
}
}Dalam MVVM, state yang tidak konsisten sering muncul sebagai bug intermiten yang sulit direproduksi. Dalam MVI, jika state tidak valid, maka tidak valid secara deterministik.
Kemampuan Pengujian Arsitektur
Kedua arsitektur dapat diuji, tetapi MVI menawarkan keunggulan signifikan melalui kemampuan prediksinya.
// MVVM test: requires verifying multiple flows
@Test
fun `loadUsers should update state correctly`() = runTest {
val viewModel = MvvmViewModel(fakeRepository)
// Observe multiple flows simultaneously
val users = mutableListOf<List<User>>()
val loadingStates = mutableListOf<Boolean>()
val job1 = launch { viewModel.users.toList(users) }
val job2 = launch { viewModel.isLoading.toList(loadingStates) }
viewModel.loadUsers()
advanceUntilIdle()
// Assertions on different flows
assertThat(users.last()).isEqualTo(expectedUsers)
assertThat(loadingStates).containsExactly(false, true, false)
job1.cancel()
job2.cancel()
}
// MVI test: single flow to verify, clear state sequence
@Test
fun `LoadUsers intent should produce correct state sequence`() = runTest {
val viewModel = MviViewModel(fakeRepository)
// Collect all states in order
val states = mutableListOf<UsersState>()
val job = launch { viewModel.state.toList(states) }
// Send the intent
viewModel.onIntent(UsersIntent.LoadUsers)
advanceUntilIdle()
// Verify the exact state sequence
assertThat(states).containsExactly(
UsersState(), // Initial
UsersState(isLoading = true), // Loading
UsersState(users = expectedUsers, isLoading = false) // Success
)
job.cancel()
}MVI memungkinkan pengujian urutan transisi state yang tepat, yang sangat berguna untuk layar kompleks dengan banyak interaksi.
Kompleksitas dan Boilerplate
Perlu jujur tentang pertukaran yang ada. MVI membutuhkan lebih banyak kode boilerplate dan pemahaman konsep yang lebih mendalam.
// MVVM: quick start, less code
class SimpleViewModel : ViewModel() {
private val _name = MutableStateFlow("")
val name: StateFlow<String> = _name.asStateFlow()
fun updateName(newName: String) {
_name.value = newName
}
}
// Total: ~10 lines
// MVI: more structure, more code
class SimpleMviViewModel : ViewModel() {
private val _state = MutableStateFlow(SimpleState())
val state: StateFlow<SimpleState> = _state.asStateFlow()
fun onIntent(intent: SimpleIntent) {
when (intent) {
is SimpleIntent.UpdateName -> {
_state.update { it.copy(name = intent.name) }
}
}
}
}
data class SimpleState(val name: String = "")
sealed class SimpleIntent {
data class UpdateName(val name: String) : SimpleIntent()
}
// Total: ~20 linesUntuk layar sederhana, MVI mungkin terasa berlebihan. Tetapi struktur ini memberikan manfaat seiring bertambahnya kompleksitas layar.
Siap menguasai wawancara Android Anda?
Berlatih dengan simulator interaktif, flashcards, dan tes teknis kami.
Kapan Memilih MVVM?
MVVM tetap menjadi pilihan pragmatis dalam beberapa situasi:
Proyek yang Sudah Ada
Jika aplikasi sudah menggunakan MVVM, migrasi ke MVI membutuhkan upaya yang besar. Memperbaiki struktur MVVM yang ada sering kali merupakan keputusan yang lebih bijak.
Tim Junior atau Campuran
MVVM lebih mudah diakses. Tim dengan pengembang pemula akan lebih cepat produktif dengan MVVM daripada MVI.
Layar Sederhana
Untuk layar dengan sedikit state dan interaksi, MVI menambah kompleksitas tanpa manfaat yang proporsional.
// For a simple settings screen, MVVM is plenty
class SettingsViewModel(
private val preferencesRepository: PreferencesRepository
) : ViewModel() {
val darkMode = preferencesRepository.darkModeFlow
.stateIn(viewModelScope, SharingStarted.Lazily, false)
val notificationsEnabled = preferencesRepository.notificationsFlow
.stateIn(viewModelScope, SharingStarted.Lazily, true)
fun toggleDarkMode() {
viewModelScope.launch {
preferencesRepository.setDarkMode(!darkMode.value)
}
}
fun toggleNotifications() {
viewModelScope.launch {
preferencesRepository.setNotifications(!notificationsEnabled.value)
}
}
}Kapan Memilih MVI?
MVI membuktikan nilainya dalam konteks tertentu:
Aplikasi dengan State yang Kompleks
Ketika layar memiliki banyak state yang saling bergantung, MVI menjamin konsistensi.
// Checkout screen with complex state: MVI excels
data class CheckoutState(
val cartItems: List<CartItem> = emptyList(),
val selectedAddress: Address? = null,
val selectedPayment: PaymentMethod? = null,
val promoCode: PromoCode? = null,
val deliveryOptions: List<DeliveryOption> = emptyList(),
val selectedDelivery: DeliveryOption? = null,
val subtotal: Money = Money.ZERO,
val discount: Money = Money.ZERO,
val deliveryFee: Money = Money.ZERO,
val total: Money = Money.ZERO,
val isLoading: Boolean = false,
val error: CheckoutError? = null,
val step: CheckoutStep = CheckoutStep.CART
) {
// Verifiable invariants
init {
require(total == subtotal - discount + deliveryFee) {
"Total inconsistent with components"
}
}
}
sealed class CheckoutIntent {
data class AddItem(val item: CartItem) : CheckoutIntent()
data class RemoveItem(val itemId: String) : CheckoutIntent()
data class SelectAddress(val address: Address) : CheckoutIntent()
data class SelectPayment(val method: PaymentMethod) : CheckoutIntent()
data class ApplyPromo(val code: String) : CheckoutIntent()
object RemovePromo : CheckoutIntent()
data class SelectDelivery(val option: DeliveryOption) : CheckoutIntent()
object ProceedToPayment : CheckoutIntent()
object ConfirmOrder : CheckoutIntent()
}Aplikasi Real-time
Untuk aplikasi dengan WebSocket, notifikasi push, atau sinkronisasi real-time, MVI mengelola beberapa aliran data dengan elegan.
Persyaratan Debugging yang Ketat
Di domain yang diregulasi (fintech, kesehatan), kemampuan untuk mereproduksi urutan kejadian secara tepat sangat berharga.
MVI memudahkan implementasi "time-travel debugging": merekam semua state dan memutar ulang sesi pengguna.
Pendekatan Hibrida: Yang Terbaik dari Dua Dunia
Dalam praktiknya, banyak tim mengadopsi pendekatan hibrida: MVI untuk layar yang kompleks, MVVM yang disederhanakan untuk layar yang sederhana. Berikut pola yang direkomendasikan:
// Base ViewModel with lightweight MVI structure
// Reusable for all screens
abstract class MviViewModel<S, I>(initialState: S) : ViewModel() {
private val _state = MutableStateFlow(initialState)
val state: StateFlow<S> = _state.asStateFlow()
protected val currentState: S get() = _state.value
// Single entry point for intents
abstract fun onIntent(intent: I)
// Helper to update state
protected fun updateState(reducer: S.() -> S) {
_state.update { it.reducer() }
}
}
// Concrete implementation stays simple
class ProfileViewModel(
private val userRepository: UserRepository
) : MviViewModel<ProfileState, ProfileIntent>(ProfileState()) {
override fun onIntent(intent: ProfileIntent) {
when (intent) {
is ProfileIntent.Load -> load(intent.userId)
is ProfileIntent.Refresh -> refresh()
is ProfileIntent.ToggleFavorite -> toggleFavorite()
}
}
private fun load(userId: String) {
viewModelScope.launch {
updateState { copy(isLoading = true) }
val user = userRepository.getUser(userId)
updateState {
copy(user = user, isLoading = false)
}
}
}
private fun refresh() = load(currentState.user?.id ?: return)
private fun toggleFavorite() {
updateState {
copy(user = user?.copy(isFavorite = !user.isFavorite))
}
}
}Pendekatan ini menawarkan manfaat MVI (state tunggal, intent yang diketik) tanpa boilerplate yang berlebihan.
Rekomendasi untuk 2026
Berikut rekomendasi untuk memilih antara dua arsitektur berdasarkan konteks:
Untuk Proyek Baru dengan Compose
Adopsi MVI dari awal. Compose dan MVI berbagi filosofi yang sama, dan investasi awal terbayar dengan cepat.
Untuk Proyek Berbasis View yang Sudah Ada
Tetap dengan MVVM, tetapi secara bertahap adopsi praktik terbaik MVI: state tunggal di ViewModel, aksi yang diketik dengan sealed class.
Untuk Tim yang Besar
Standardisasi pada satu pendekatan dan dokumentasikan. Konsistensi di seluruh kode lebih penting daripada pilihan pola itu sendiri.
Pola terbaik adalah yang dipahami dan diterapkan dengan benar oleh tim. MVVM yang diimplementasikan dengan baik mengalahkan MVI yang tidak dipahami dengan baik.
Kesimpulan
MVVM dan MVI keduanya merupakan pendekatan yang valid untuk mengarsitektur aplikasi Android. MVVM menawarkan kesederhanaan dan keakraban, sementara MVI membawa prediktabilitas dan debugging yang lebih mudah.
Daftar Periksa Keputusan
- Pilih MVVM jika: tim junior, proyek sederhana, migrasi mahal
- Pilih MVI jika: Compose native, state kompleks, debugging kritis
- Pendekatan hibrida direkomendasikan: MVI ringan dengan state tunggal, tanpa over-engineering
- Prioritas tertinggi: konsistensi di seluruh kode
Mulai berlatih!
Uji pengetahuan Anda dengan simulator wawancara dan tes teknis kami.
Apapun pilihannya, kunci utama adalah memahami kekuatan dan kelemahan setiap pendekatan untuk membuat keputusan yang tepat. Kode terbaik adalah kode yang dapat dipelihara oleh tim dengan tenang dalam jangka panjang.
Tag
Bagikan
Artikel terkait

Menguasai Kotlin Coroutines: Panduan Lengkap 2026
Pelajari Kotlin coroutines untuk pengembangan Android: suspend function, scope, dispatcher, dan pola-pola tingkat lanjut.

React Native: Membangun Aplikasi Mobile Lengkap di Tahun 2026
Panduan lengkap pengembangan aplikasi mobile iOS dan Android dengan React Native. Dari persiapan lingkungan hingga publikasi, semua dasar yang Anda perlukan untuk memulai.