MVVM vs MVI บน Android: เลือก Architecture ไหนดีในปี 2026?

เปรียบเทียบ MVVM และ MVI บน Android อย่างละเอียด: ข้อดีข้อเสีย กรณีการใช้งาน และคู่มือปฏิบัติในการเลือก architecture ที่เหมาะสมในปี 2026

การเปรียบเทียบ architecture MVVM และ MVI สำหรับ Android

การเลือก architecture ที่เหมาะสมเป็นการตัดสินใจสำคัญที่ส่งผลต่อความสามารถในการบำรุงรักษา การทดสอบ และความสามารถในการขยายตัวของแอปพลิเคชัน Android ในปี 2026 มีสองรูปแบบที่ครอบงำระบบนิเวศ: MVVM ซึ่งเป็นมาตรฐานอุตสาหกรรม และ MVI แนวทาง reactive ที่ได้รับความนิยมเพิ่มขึ้นควบคู่กับ Jetpack Compose

ความสำคัญของการเลือก

การเลือก architecture ที่ผิดพลาดมีต้นทุนสูง: หนี้ทางเทคนิค บั๊กที่ยากต่อการจำลอง และการ refactor ที่เจ็บปวด การทำความเข้าใจจุดแข็งและจุดอ่อนของแต่ละแนวทางช่วยประหยัดปัญหาในระยะยาวได้มาก

ทำความเข้าใจ MVVM: มาตรฐานที่ยั่งยืน

MVVM (Model-View-ViewModel) คือ architecture ที่ Google แนะนำตั้งแต่เปิดตัว Jetpack โดยแยกความรับผิดชอบออกเป็นสามชั้นที่ชัดเจน ทำให้โค้ดมีระเบียบและทดสอบได้ง่ายขึ้น

หลักการพื้นฐานของ MVVM

รูปแบบ MVVM อาศัยการแยกส่วนที่ชัดเจน: Model จัดการข้อมูลและ business logic, View แสดง UI และ ViewModel เชื่อมทั้งสองเข้าด้วยกันโดยนำเสนอ state ที่สังเกตได้

ด้านล่างเป็นการ implement หน้าโปรไฟล์ผู้ใช้ด้วย MVVM ตัวอย่างแรกนี้แสดงโครงสร้างพื้นฐานของ ViewModel ที่เปิดเผย state ที่สังเกตได้และ method สำหรับการโต้ตอบของผู้ใช้

UserProfileViewModel.ktkotlin
// 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 นี้แสดงแนวทาง MVVM ทั่วไป: หลาย flow ที่สังเกตได้ (state หลัก การโหลด ข้อผิดพลาด) และ method สาธารณะสำหรับแต่ละ action ของผู้ใช้

ข้อดีของ MVVM

MVVM มีจุดแข็งหลายประการที่อธิบายการนำไปใช้อย่างแพร่หลาย:

  • ความคุ้นเคย: นักพัฒนา Android ส่วนใหญ่รู้จักรูปแบบนี้
  • ความยืดหยุ่น: สามารถจัดโครงสร้าง state ได้ตามต้องการ
  • ระบบนิเวศ: รวมเข้ากับ Jetpack ได้อย่างสมบูรณ์ (LiveData, StateFlow, Hilt)
  • ความเรียบง่าย: เส้นโค้งการเรียนรู้ที่ไม่ชันสำหรับผู้เริ่มต้น

MVVM เหมาะอย่างยิ่งสำหรับทีมผสมที่มีนักพัฒนาระดับต่างๆ ความเรียบง่ายทางแนวคิดช่วยให้การ onboarding ง่ายขึ้น

ข้อจำกัดของ MVVM

อย่างไรก็ตาม MVVM แสดงข้อจำกัดเมื่อแอปพลิเคชันเติบโตขึ้น ปัญหาหลักคือการจัดการ state แบบกระจาย ตัวอย่างต่อไปนี้แสดงปัญหาที่พบบ่อยนี้:

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

ตัวอย่างนี้แสดงให้เห็นว่า state สามารถแตกกระจายใน MVVM ได้อย่างไร ทำให้การติดตามการเปลี่ยนแปลงและการจำลองบั๊กทำได้ยากขึ้น

ทำความเข้าใจ MVI: แนวทางทิศทางเดียว

MVI (Model-View-Intent) ใช้ปรัชญาที่แตกต่างออกไป: การไหลของข้อมูลทิศทางเดียวและ state เดียวที่ไม่สามารถเปลี่ยนแปลงได้ แนวทางที่ได้รับแรงบันดาลใจจาก Redux นี้ขจัดปัญหา state ที่ไม่สอดคล้องกัน

หลักการพื้นฐานของ MVI

ใน MVI ทุกอย่างเป็นไปตามวงจรที่ชัดเจน: ผู้ใช้ส่ง Intent (action), Reducer แปลง state ปัจจุบันเป็น state ใหม่ และ View แสดง state เดียวนั้น ทำนายได้ ทดสอบได้ และ debug ได้

ด้านล่างเป็นการ implement หน้าโปรไฟล์เดิมด้วย MVI สังเกตว่า state รวมศูนย์อยู่อย่างไรและ action ถูกกำหนดประเภทอย่างชัดเจน

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

ความแตกต่างชัดเจน: state flow เดียว action ที่ชัดเจน และการแยกส่วนที่สะอาดระหว่าง state ถาวรและ effect ครั้งเดียว

การ Debug ที่ง่ายขึ้น

ด้วย MVI สามารถบันทึก Intent ทุกรายการและการเปลี่ยนแปลง state ทุกครั้งได้ การจำลองบั๊กกลายเป็นเรื่องง่าย: เพียงเล่นซ้ำลำดับ Intent

MVI กับ Jetpack Compose

MVI โดดเด่นเป็นพิเศษกับ Jetpack Compose เนื่องจากทั้งสองแบ่งปันปรัชญาเดียวกัน: state ที่ไม่สามารถเปลี่ยนแปลงและ UI แบบ declarative ต่อไปนี้คือวิธีเชื่อมต่อ ViewModel กับหน้าจอ 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) }
                )
            }
        }
    }
}

UI กลายเป็น pure function ของ state: ทำนายได้ ทดสอบได้ และไม่มี side effect ที่ซ่อนอยู่

การเปรียบเทียบโดยละเอียด

หลังจากดูทั้งสองรูปแบบในทางปฏิบัติแล้ว สามารถเปรียบเทียบตามเกณฑ์ที่สำคัญจริงๆ ในสภาพแวดล้อมการผลิต

การจัดการ State

ความแตกต่างพื้นฐานอยู่ที่การจัดการ state ความแตกต่างนี้ส่งผลโดยตรงต่อความสามารถในการบำรุงรักษาในระยะยาว

StateComparison.ktkotlin
// 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 }
            )
        }
    }
}
บั๊กผีสิง

ใน MVVM state ที่ไม่สอดคล้องกันมักแสดงออกมาเป็นบั๊กที่เกิดขึ้นไม่สม่ำเสมอและยากต่อการจำลอง ใน MVI หาก state ไม่ถูกต้องก็ไม่ถูกต้องอย่างแน่นอน

ความสามารถในการทดสอบ Architecture

ทั้งสอง architecture สามารถทดสอบได้ แต่ MVI มีข้อได้เปรียบสำคัญด้วยความสามารถในการทำนาย

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 ช่วยให้ทดสอบลำดับการเปลี่ยน state ที่แน่นอนได้ ซึ่งมีประโยชน์อย่างยิ่งสำหรับหน้าจอที่ซับซ้อนที่มีการโต้ตอบมาก

ความซับซ้อนและโค้ด Boilerplate

ควรซื่อสัตย์เกี่ยวกับการแลกเปลี่ยน MVI ต้องการโค้ด boilerplate มากกว่าและความเข้าใจแนวคิดที่ลึกซึ้งกว่า

BoilerplateComparison.ktkotlin
// 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 lines

สำหรับหน้าจอง่ายๆ MVI อาจดูเกินความจำเป็น แต่โครงสร้างนี้จะให้ผลตอบแทนเมื่อหน้าจอมีความซับซ้อนเพิ่มขึ้น

พร้อมที่จะพิชิตการสัมภาษณ์ Android แล้วหรือยังครับ?

ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ

เมื่อไหรควรเลือก MVVM?

MVVM ยังคงเป็นตัวเลือกที่ใช้งานได้จริงในหลายสถานการณ์:

โปรเจกต์ที่มีอยู่แล้ว

หากแอปพลิเคชันใช้ MVVM อยู่แล้ว การย้ายไปยัง MVI ต้องใช้ความพยายามอย่างมาก การปรับปรุงโครงสร้าง MVVM ที่มีอยู่มักเป็นการตัดสินใจที่ฉลาดกว่า

ทีม Junior หรือทีมผสม

MVVM เข้าถึงได้ง่ายกว่า ทีมที่มีนักพัฒนามือใหม่จะมีประสิทธิผลเร็วกว่าด้วย MVVM มากกว่า MVI

หน้าจอที่เรียบง่าย

สำหรับหน้าจอที่มี state และการโต้ตอบน้อย MVI เพิ่มความซับซ้อนโดยไม่มีประโยชน์ที่สมดุล

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

เมื่อไหรควรเลือก MVI?

MVI แสดงคุณค่าในบริบทเฉพาะ:

แอปพลิเคชันที่มี State ซับซ้อน

เมื่อหน้าจอมี state ที่พึ่งพากันหลายอย่าง MVI รับประกันความสอดคล้อง

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()
    object RemovePromo : CheckoutIntent()
    data class SelectDelivery(val option: DeliveryOption) : CheckoutIntent()
    object ProceedToPayment : CheckoutIntent()
    object ConfirmOrder : CheckoutIntent()
}

แอปพลิเคชัน Real-time

สำหรับแอปที่มี WebSocket การแจ้งเตือน push หรือการซิงค์แบบ real-time MVI จัดการหลาย data flow ได้อย่างสง่างาม

ข้อกำหนดการ Debug ที่เข้มงวด

ในโดเมนที่มีการควบคุม (fintech สุขภาพ) ความสามารถในการจำลองลำดับเหตุการณ์อย่างแม่นยำมีค่ามาก

MVI ช่วยให้การ implement "time-travel debugging" ง่ายขึ้น: บันทึก state ทั้งหมดและเล่นซ้ำ session ของผู้ใช้

แนวทาง Hybrid: ดีที่สุดจากทั้งสองโลก

ในทางปฏิบัติ ทีมจำนวนมากใช้แนวทาง hybrid: MVI สำหรับหน้าจอซับซ้อน MVVM ที่เรียบง่ายสำหรับหน้าจอง่าย ต่อไปนี้เป็นรูปแบบที่แนะนำ:

MviViewModel.ktkotlin
// 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))
        }
    }
}

แนวทางนี้มอบประโยชน์ของ MVI (state เดียว intent ที่กำหนดประเภท) โดยไม่มีโค้ด boilerplate มากเกินไป

คำแนะนำสำหรับปี 2026

ต่อไปนี้คือคำแนะนำสำหรับการเลือกระหว่างสอง architecture ตามบริบท:

สำหรับโปรเจกต์ใหม่ที่ใช้ Compose

นำ MVI มาใช้ตั้งแต่เริ่มต้น Compose และ MVI ใช้ปรัชญาเดียวกัน และการลงทุนเริ่มต้นจะตอบแทนคืนอย่างรวดเร็ว

สำหรับโปรเจกต์ที่ใช้ View อยู่แล้ว

ยังคงใช้ MVVM แต่ค่อยๆ นำแนวทางปฏิบัติที่ดีที่สุดของ MVI มาใช้: state เดียวใน ViewModel action ที่กำหนดประเภทด้วย sealed class

สำหรับทีมขนาดใหญ่

กำหนดมาตรฐานเป็นแนวทางเดียวและจัดทำเอกสาร ความสม่ำเสมอในโค้ดสำคัญกว่าการเลือก pattern เอง

เกณฑ์ที่แท้จริง

รูปแบบที่ดีที่สุดคือรูปแบบที่ทีมเข้าใจและนำไปใช้อย่างถูกต้อง MVVM ที่ implement ได้ดีนั้นดีกว่า MVI ที่เข้าใจได้ไม่ดี

สรุป

MVVM และ MVI เป็นแนวทางที่ถูกต้องทั้งคู่สำหรับการออกแบบ architecture ของแอปพลิเคชัน Android MVVM มอบความเรียบง่ายและความคุ้นเคย ในขณะที่ MVI นำมาซึ่งความสามารถในการทำนายและการ debug ที่ง่ายขึ้น

รายการตรวจสอบการตัดสินใจ

  • เลือก MVVM หาก: ทีม junior โปรเจกต์ง่าย การย้ายระบบมีต้นทุนสูง
  • เลือก MVI หาก: Compose native state ซับซ้อน ต้องการ debug เชิงวิกฤต
  • แนะนำแนวทาง hybrid: MVI น้ำหนักเบาที่มี state เดียว ไม่มีการ over-engineering
  • ลำดับความสำคัญสูงสุด: ความสม่ำเสมอทั่วทั้ง codebase

เริ่มฝึกซ้อมเลย!

ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ

ไม่ว่าจะเลือกอะไร กุญแจสำคัญคือการเข้าใจจุดแข็งและจุดอ่อนของแต่ละแนวทางเพื่อการตัดสินใจที่มีข้อมูลสนับสนุน โค้ดที่ดีที่สุดคือโค้ดที่ทีมสามารถบำรุงรักษาได้อย่างสงบในระยะยาว

แท็ก

#android
#mvvm
#mvi
#architecture
#jetpack compose

แชร์

บทความที่เกี่ยวข้อง