MVVM vs MVI: Kiến Trúc Nào Phù Hợp 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. Cập nhật với explicit backing fields Kotlin 2.4.

So sánh kiến trúc MVVM và MVI cho Android

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, và MVI, phương pháp phản ứng đã trở thành lựa chọn tự nhiên cho 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.

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. Chú ý cách explicit backing fields trong Kotlin 2.4 loại bỏ mẫu boilerplate truyền thống _uiState / uiState.

UserProfileViewModel.ktkotlin
// MVVM ViewModel with Kotlin 2.4 explicit backing fields
// The backing field syntax eliminates the _state / state dance
class UserProfileViewModel(
    private val userRepository: UserRepository,
    private val analyticsTracker: AnalyticsTracker
) : ViewModel() {

    // Kotlin 2.4: explicit backing field - no more _uiState / uiState pair
    val uiState: StateFlow<UserProfileState>
        field = MutableStateFlow(UserProfileState())

    // Separate loading state - MVVM allows multiple flows
    val isLoading: StateFlow<Boolean>
        field = MutableStateFlow(false)

    // One-shot error messages via SharedFlow
    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 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. Cú pháp explicit backing fields trong Kotlin 2.4 giúp code gọn gàng hơn bằng cách khai báo kiểu thuộc tính là StateFlow trong khi backing field là MutableStateFlow.

Ư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 về trạng thái bị phân mảnh:

ProblematicViewModel.ktkotlin
// 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
    val cart: StateFlow<List<CartItem>>
        field = MutableStateFlow(emptyList())
    
    val selectedAddress: StateFlow<Address?>
        field = MutableStateFlow(null)
    
    val selectedPayment: StateFlow<PaymentMethod?>
        field = MutableStateFlow(null)
    
    val promoCode: StateFlow<String?>
        field = MutableStateFlow(null)
    
    val isLoading: StateFlow<Boolean>
        field = MutableStateFlow(false)
    
    val error: StateFlow<String?>
        field = MutableStateFlow(null)

    // Each modification can create temporary inconsistent states
    fun applyPromoCode(code: String) {
        viewModelScope.launch {
            isLoading.value = true
            error.value = null

            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.

Triển khai cùng màn hình hồ sơ người dùng này cho thấy 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:

UserProfileMviViewModel.ktkotlin
// 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
    val state: StateFlow<UserProfileState>
        field = MutableStateFlow(UserProfileState())

    // 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()
    data object EnableEditMode : UserProfileIntent()
    data class SaveProfile(val name: String, val bio: String) : UserProfileIntent()
    data 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:

UserProfileScreen.ktkotlin
// 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 cả hai mẫu đã rõ ràng, dưới đây là so sánh 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.

StateComparison.ktkotlin
// MVVM: potentially fragmented state
class MvvmViewModel : ViewModel() {
    // Multiple sources of truth - manual synchronization needed
    val users: StateFlow<List<User>>
        field = MutableStateFlow(emptyList())
    
    val selectedUser: StateFlow<User?>
        field = MutableStateFlow(null)
    
    val isLoading: StateFlow<Boolean>
        field = MutableStateFlow(false)
    
    val searchQuery: StateFlow<String>
        field = 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
    val state: StateFlow<UsersState>
        field = 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 }
            )
        }
    }
}
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ó.

TestComparison.ktkotlin
// 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 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

Kể từ Kotlin 2.4, khoảng cách boilerplate giữa MVVM và MVI đã thu hẹp đáng kể. Tính năng explicit backing fields loại bỏ mẫu _state / state vốn trước đây thêm dòng code vào mọi ViewModel.

BoilerplateComparison.ktkotlin
// MVVM: quick start, less code
class SimpleViewModel : ViewModel() {
    val name: StateFlow<String>
        field = MutableStateFlow("")

    fun updateName(newName: String) {
        name.value = newName
    }
}
// Total: ~8 lines

// MVI: more structure, more code
class SimpleMviViewModel : ViewModel() {
    val state: StateFlow<SimpleState>
        field = MutableStateFlow(SimpleState())

    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: ~18 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.

Sẵn sàng chinh phục phỏng vấn Android?

Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.

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.

SettingsViewModel.ktkotlin
// 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.

CheckoutState.ktkotlin
// 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()
    data object RemovePromo : CheckoutIntent()
    data class SelectDelivery(val option: DeliveryOption) : CheckoutIntent()
    data object ProceedToPayment : CheckoutIntent()
    data 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.

Lựa Chọn Thay Thế Mới Nổi: Circuit

Circuit, được phát triển bởi Slack, cung cấp một triển khai MVI native cho Compose đáng cân nhắc cho các dự án mới. Circuit kết hợp presenter với Molecule bên trong, loại bỏ phần lớn boilerplate MVI trong khi vẫn giữ luồng dữ liệu một chiều.

Circuit đặc biệt thú vị cho các dự án Kotlin Multiplatform vì hỗ trợ chia sẻ logic presentation xuyên nền tảng. Tuy nhiên, mức độ áp dụng ngoài Slack vẫn còn hạn chế so với cách tiếp cận chuẩn Jetpack ViewModel.

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ị:

MviViewModel.ktkotlin
// Base ViewModel with lightweight MVI structure
// Reusable for all screens
abstract class MviViewModel<S, I>(initialState: S) : ViewModel() {

    val state: StateFlow<S>
        field = MutableStateFlow(initialState)

    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))
        }
    }
}

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ị

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. Với explicit backing fields của Kotlin 2.4, khoảng cách boilerplate đã thu hẹp.

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.

Nguồn Tham Khảo

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

Bắt đầu luyện tập!

Kiểm tra kiến thức với mô phỏng phỏng vấn và bài kiểm tra kỹ thuật.

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.

Thử thách hôm nay

Bạn có tìm ra lỗi trong Android không?

Một đoạn mã thật, một lỗi ẩn, mỗi ngày một lượt. Không cần tài khoản để thử.

Anthony Fillion-Maillet

Viết bởi

Anthony Fillion-Maillet

Người sáng lập SharpSkill

Lập trình viên fullstack hơn 10 năm. Anh điều hành SharpSkill và chịu trách nhiệm về mọi nội dung đăng tại đây.

Cập nhật ngày 19 tháng 8, 2026

Thẻ

#android
#mvvm
#mvi
#architecture
#jetpack compose

Chia sẻ

Bài viết liên quan