# MVVM vs MVI: Kiến Trúc Nào Phù Hợp Cho Android Năm 2026? > So sánh chuyên sâu MVVM và MVI trên Android: ưu điểm, nhược điểm, trường hợp sử dụng và hướng dẫn thực tiễn để lựa chọn kiến trúc phù hợp trong năm 2026. - Published: 2026-01-06 - Updated: 2026-04-05 - Author: SharpSkill - Tags: android, mvvm, mvi, architecture, jetpack compose - Reading time: 11 min --- Việc lựa chọn kiến trúc phù hợp là quyết định then chốt ảnh hưởng trực tiếp đến khả năng bảo trì, kiểm thử và mở rộng của ứng dụng Android. Trong năm 2026, hai mẫu thiết kế chiếm ưu thế trong hệ sinh thái: MVVM — tiêu chuẩn công nghiệp đã được kiểm chứng — và MVI — phương pháp phản ứng đang ngày càng phổ biến cùng với Jetpack Compose. > **Lựa Chọn Kiến Trúc Quan Trọng Hơn Bạn Nghĩ** > > Một lựa chọn kiến trúc sai lầm rất tốn kém: nợ kỹ thuật chồng chất, lỗi khó tái hiện và việc tái cấu trúc đau đầu. Hiểu rõ điểm mạnh và điểm yếu của từng phương pháp sẽ giúp tiết kiệm rất nhiều công sức về sau. ## MVVM: Tiêu Chuẩn Đã Được Thiết Lập MVVM (Model-View-ViewModel) là kiến trúc được Google khuyến nghị kể từ khi giới thiệu Jetpack. Mẫu này phân tách trách nhiệm thành ba tầng rõ ràng, giúp code có tổ chức hơn và dễ kiểm thử hơn. ### Nguyên Tắc Cốt Lõi Của MVVM Mẫu MVVM dựa trên sự phân tách rõ ràng: Model xử lý dữ liệu và logic nghiệp vụ, View hiển thị giao diện người dùng, và ViewModel làm cầu nối giữa hai tầng bằng cách cung cấp các trạng thái có thể quan sát được. Dưới đây là cách triển khai màn hình hồ sơ người dùng với MVVM. Ví dụ đầu tiên minh họa cấu trúc cơ bản với ViewModel hiển thị trạng thái có thể quan sát và các phương thức xử lý tương tác của người dùng. ```kotlin // UserProfileViewModel.kt // 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 = _uiState.asStateFlow() // Separate loading state - MVVM allows multiple flows private val _isLoading = MutableStateFlow(false) val isLoading: StateFlow = _isLoading.asStateFlow() // One-shot error messages private val _errorMessage = MutableSharedFlow() val errorMessage: SharedFlow = _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 này minh họa cách tiếp cận MVVM điển hình: nhiều flow có thể quan sát (trạng thái chính, loading, lỗi) và các phương thức công khai cho từng hành động của người dùng. ### Ưu Điểm Của MVVM MVVM có một số điểm mạnh giải thích cho mức độ phổ biến rộng rãi của nó: - **Quen thuộc**: Hầu hết các nhà phát triển Android đều biết mẫu này - **Linh hoạt**: Có thể cấu trúc trạng thái theo cách mong muốn - **Hệ sinh thái**: Tích hợp hoàn hảo với Jetpack (LiveData, StateFlow, Hilt) - **Đơn giản**: Đường cong học tập nhẹ nhàng cho người mới bắt đầu > MVVM đặc biệt phù hợp với các nhóm hỗn hợp có nhiều mức độ kinh nghiệm khác nhau. Tính đơn giản về mặt khái niệm giúp việc onboarding diễn ra thuận lợi hơn. ### Hạn Chế Của MVVM Tuy nhiên, MVVM bộc lộ hạn chế khi ứng dụng ngày càng phình to. Vấn đề chính là quản lý trạng thái phân tán. Ví dụ dưới đây minh họa vấn đề phổ biến này: ```kotlin // ProblematicViewModel.kt // 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>(emptyList()) private val _selectedAddress = MutableStateFlow(null) private val _selectedPayment = MutableStateFlow(null) private val _promoCode = MutableStateFlow(null) private val _isLoading = MutableStateFlow(false) private val _error = MutableStateFlow(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... } } ``` Ví dụ này cho thấy trạng thái có thể bị phân mảnh trong MVVM như thế nào, khiến việc theo dõi các chuyển đổi trạng thái và tái hiện lỗi trở nên khó khăn. ## MVI: Phương Pháp Luồng Dữ Liệu Một Chiều MVI (Model-View-Intent) áp dụng triết lý khác: luồng dữ liệu một chiều và một trạng thái bất biến duy nhất. Cách tiếp cận này, lấy cảm hứng từ Redux, loại bỏ các vấn đề về trạng thái không nhất quán. ### Nguyên Tắc Cốt Lõi Của MVI Trong MVI, mọi thứ đều tuân theo một chu trình rõ ràng: người dùng phát ra **Intent** (hành động), **Reducer** biến đổi trạng thái hiện tại thành trạng thái mới, và **View** hiển thị trạng thái duy nhất đó. Chu trình này có tính dự đoán được, dễ kiểm thử và dễ gỡ lỗi. Dưới đây là cách triển khai cùng màn hình hồ sơ người dùng, lần này với MVI. Chú ý cách trạng thái được tập trung hóa và các hành động được định kiểu rõ ràng. ```kotlin // UserProfileMviViewModel.kt // 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 = _state.asStateFlow() // Channel for side effects (navigation, snackbar) private val _sideEffect = Channel() val sideEffect: Flow = _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() } ``` Sự khác biệt rõ ràng: một flow trạng thái duy nhất, các hành động được định kiểu tường minh và sự phân tách gọn gàng giữa trạng thái bền vững và hiệu ứng phụ tức thời. > **Gỡ Lỗi Dễ Dàng Hơn** > > Với MVI, có thể ghi lại mọi Intent và mọi chuyển đổi trạng thái. Việc tái hiện lỗi trở nên đơn giản: chỉ cần phát lại chuỗi Intent đã xảy ra. ### MVI Kết Hợp Với Jetpack Compose MVI tỏa sáng đặc biệt khi kết hợp với Jetpack Compose, bởi cả hai cùng chia sẻ triết lý: trạng thái bất biến và giao diện khai báo. Dưới đây là cách kết nối ViewModel với màn hình Compose: ```kotlin // UserProfileScreen.kt // 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) } ) } } } } ``` Giao diện người dùng trở thành hàm thuần túy của trạng thái: có thể dự đoán, dễ kiểm thử và không có hiệu ứng phụ ẩn. ## So Sánh Chi Tiết Sau khi đã thấy cả hai mẫu hoạt động thực tế, hãy so sánh chúng theo các tiêu chí thực sự quan trọng trong môi trường sản xuất. ### Quản Lý Trạng Thái Sự khác biệt căn bản nằm ở cách quản lý trạng thái. Sự phân biệt này ảnh hưởng trực tiếp đến khả năng bảo trì dài hạn. ```kotlin // StateComparison.kt // MVVM: potentially fragmented state class MvvmViewModel : ViewModel() { // Multiple sources of truth - manual synchronization needed private val _users = MutableStateFlow>(emptyList()) private val _selectedUser = MutableStateFlow(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 = 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 } ) } } } ``` > **Lỗi Ma** > > Trong MVVM, các trạng thái không nhất quán thường biểu hiện thành những lỗi không liên tục, khó tái hiện. Trong MVI, nếu trạng thái không hợp lệ, nó sẽ không hợp lệ một cách xác định. ### Khả Năng Kiểm Thử Kiến Trúc Cả hai kiến trúc đều có thể kiểm thử được, nhưng MVI mang lại lợi thế đáng kể nhờ tính dự đoán của nó. ```kotlin // TestComparison.kt // 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>() val loadingStates = mutableListOf() 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() 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 cho phép kiểm thử chuỗi chuyển đổi trạng thái chính xác, điều này đặc biệt hữu ích cho các màn hình phức tạp với nhiều tương tác. ### Độ Phức Tạp Và Code Lặp Lại Cần thừa nhận thẳng thắn về sự đánh đổi. MVI đòi hỏi nhiều code boilerplate hơn và hiểu biết sâu hơn về các khái niệm liên quan. ```kotlin // BoilerplateComparison.kt // MVVM: quick start, less code class SimpleViewModel : ViewModel() { private val _name = MutableStateFlow("") val name: StateFlow = _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 = _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 lines ``` Với một màn hình đơn giản, MVI có vẻ thái quá. Nhưng cấu trúc này mang lại lợi tức đầu tư khi màn hình ngày càng phức tạp hơn. ## Khi Nào Nên Chọn MVVM? MVVM vẫn là lựa chọn thực dụng trong một số tình huống cụ thể: ### Dự Án Hiện Có Nếu ứng dụng đã sử dụng MVVM, việc chuyển sang MVI đòi hỏi nỗ lực đáng kể. Cải thiện cấu trúc MVVM hiện có thường là quyết định khôn ngoan hơn. ### Nhóm Mới Bắt Đầu Hoặc Hỗn Hợp MVVM dễ tiếp cận hơn. Một nhóm có các nhà phát triển mới bắt đầu sẽ đạt năng suất nhanh hơn với MVVM so với MVI. ### Màn Hình Đơn Giản Với các màn hình ít trạng thái và tương tác, MVI tăng thêm độ phức tạp mà không mang lại lợi ích tương xứng. ```kotlin // SettingsViewModel.kt // 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) } } } ``` ## Khi Nào Nên Chọn MVI? MVI thể hiện giá trị thực sự trong các bối cảnh cụ thể: ### Ứng Dụng Có Trạng Thái Phức Tạp Khi một màn hình có nhiều trạng thái phụ thuộc lẫn nhau, MVI đảm bảo tính nhất quán. ```kotlin // CheckoutState.kt // Checkout screen with complex state: MVI excels data class CheckoutState( val cartItems: List = emptyList(), val selectedAddress: Address? = null, val selectedPayment: PaymentMethod? = null, val promoCode: PromoCode? = null, val deliveryOptions: List = 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() } ``` ### Ứng Dụng Thời Gian Thực Với các ứng dụng sử dụng WebSocket, thông báo đẩy hoặc đồng bộ hóa thời gian thực, MVI xử lý nhiều luồng dữ liệu một cách thanh lịch. ### Yêu Cầu Gỡ Lỗi Nghiêm Ngặt Trong các lĩnh vực được quản lý chặt chẽ (fintech, y tế), khả năng tái hiện chính xác một chuỗi sự kiện có giá trị vô cùng lớn. > MVI giúp dễ dàng triển khai "time-travel debugging": ghi lại tất cả các trạng thái và phát lại phiên làm việc của người dùng. ## Phương Pháp Kết Hợp: Tận Dụng Điểm Mạnh Của Cả Hai Trên thực tế, nhiều nhóm áp dụng phương pháp kết hợp: MVI cho các màn hình phức tạp, MVVM đơn giản hóa cho các màn hình đơn giản. Dưới đây là mẫu được khuyến nghị: ```kotlin // MviViewModel.kt // Base ViewModel with lightweight MVI structure // Reusable for all screens abstract class MviViewModel(initialState: S) : ViewModel() { private val _state = MutableStateFlow(initialState) val state: StateFlow = _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()) { 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)) } } } ``` Cách tiếp cận này mang lại lợi ích của MVI (trạng thái duy nhất, Intent được định kiểu) mà không có code boilerplate quá nhiều. ## Khuyến Nghị Cho Năm 2026 Dưới đây là các khuyến nghị để lựa chọn giữa hai kiến trúc dựa trên bối cảnh cụ thể: ### Dự Án Mới Với Compose Hãy áp dụng MVI ngay từ đầu. Compose và MVI chia sẻ cùng triết lý, và khoản đầu tư ban đầu mang lại lợi ích nhanh chóng. ### Dự Án Hiện Có Dựa Trên View Duy trì MVVM nhưng dần dần áp dụng các phương pháp hay nhất của MVI: trạng thái duy nhất trong ViewModel, các hành động được định kiểu với sealed class. ### Nhóm Lớn Chuẩn hóa theo một phương pháp và ghi lại tài liệu đầy đủ. Tính nhất quán trong toàn bộ codebase quan trọng hơn bản thân việc chọn mẫu nào. > **Tiêu Chí Thực Sự** > > Mẫu tốt nhất là mẫu mà nhóm hiểu và áp dụng đúng cách. Một MVVM được triển khai tốt còn hiệu quả hơn một MVI không được hiểu đúng. ## Kết Luận MVVM và MVI đều là những phương pháp hợp lệ để xây dựng kiến trúc cho ứng dụng Android. MVVM mang lại sự đơn giản và quen thuộc, trong khi MVI đem đến tính dự đoán được và khả năng gỡ lỗi dễ dàng hơn. ### Danh Sách Kiểm Tra Quyết Định - Chọn MVVM nếu: nhóm mới bắt đầu, dự án đơn giản, chi phí chuyển đổi cao - Chọn MVI nếu: Compose thuần túy, trạng thái phức tạp, yêu cầu gỡ lỗi quan trọng - Phương pháp kết hợp được khuyến nghị: MVI nhẹ với trạng thái duy nhất, không over-engineering - Ưu tiên hàng đầu: tính nhất quán trên toàn bộ codebase Dù lựa chọn là gì, điều quan trọng là hiểu rõ điểm mạnh và điểm yếu của từng phương pháp để đưa ra quyết định sáng suốt. Code tốt nhất là code mà nhóm có thể bảo trì một cách thoải mái trong dài hạn. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/vi/blog/android/mvvm-vs-mvi-architecture