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

เปรียบเทียบ MVVM และ MVI บน Android อย่างละเอียด: ข้อดีข้อเสีย กรณีการใช้งาน และคู่มือปฏิบัติ อัปเดตพร้อม explicit backing fields ของ Kotlin 2.4

การเปรียบเทียบ 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 ที่สังเกตได้

ตัวอย่างแรกนี้แสดงโครงสร้างพื้นฐานของ ViewModel ที่เปิดเผย state ที่สังเกตได้และ method สำหรับการโต้ตอบของผู้ใช้ สังเกตว่า explicit backing fields ใน Kotlin 2.4 ช่วยกำจัดรูปแบบ boilerplate แบบดั้งเดิม _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 นี้แสดงแนวทาง MVVM ทั่วไป: หลาย flow ที่สังเกตได้ (state หลัก การโหลด ข้อผิดพลาด) และ method สาธารณะสำหรับแต่ละ action ของผู้ใช้ Syntax ของ explicit backing fields ใน Kotlin 2.4 ทำให้โค้ดสะอาดขึ้นโดยประกาศ property type เป็น StateFlow ในขณะที่ backing field เป็น MutableStateFlow

ข้อดีของ MVVM

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

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

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

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

อย่างไรก็ตาม MVVM แสดงข้อจำกัดเมื่อแอปพลิเคชันเติบโตขึ้น ปัญหาหลักคือการจัดการ state แบบกระจาย ตัวอย่างต่อไปนี้แสดงปัญหาที่พบบ่อยของ 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
    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...
    }
}

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

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

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

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

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

การ implement หน้าโปรไฟล์เดียวกันนี้แสดงให้เห็นว่า 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
    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()
}

ความแตกต่างชัดเจน: 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
    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 }
            )
        }
    }
}
บั๊กผีสิง

ใน 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

ตั้งแต่ Kotlin 2.4 ช่องว่าง boilerplate ระหว่าง MVVM และ MVI ได้แคบลงอย่างมาก ฟีเจอร์ explicit backing fields ช่วยกำจัดรูปแบบ _state / state ที่ก่อนหน้านี้เพิ่มบรรทัดในทุก 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

สำหรับหน้าจอง่ายๆ 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()
    data object RemovePromo : CheckoutIntent()
    data class SelectDelivery(val option: DeliveryOption) : CheckoutIntent()
    data object ProceedToPayment : CheckoutIntent()
    data object ConfirmOrder : CheckoutIntent()
}

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

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

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

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

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

ทางเลือกที่กำลังมาแรง: Circuit

Circuit ซึ่งพัฒนาโดย Slack นำเสนอการ implement MVI แบบ Compose-native ที่น่าพิจารณาสำหรับโปรเจกต์ใหม่ Circuit รวม presenter กับ Molecule ภายใน ช่วยกำจัด boilerplate ของ MVI ส่วนใหญ่ในขณะที่รักษาการไหลของข้อมูลทิศทางเดียว

Circuit น่าสนใจเป็นพิเศษสำหรับโปรเจกต์ Kotlin Multiplatform เนื่องจากรองรับการแชร์ presentation logic ข้ามแพลตฟอร์ม อย่างไรก็ตาม การนำไปใช้นอก Slack ยังมีจำกัดเมื่อเทียบกับแนวทางมาตรฐาน Jetpack ViewModel

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

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

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

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

คำแนะนำ

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

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

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

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

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

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

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

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

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

แหล่งอ้างอิง

  • Kotlin 2.4.0 What's New - Explicit backing fields เสถียรแล้ว ช่วยกำจัด boilerplate _state / state
  • Jetpack Compose August 2026 Release - Compose 1.12 พร้อม BOM 2026.08.00
  • Circuit by Slack - Architecture MVI ที่ขับเคลื่อนด้วย Compose พร้อม Molecule integration
  • Android Architecture Guide - คำแนะนำ architecture แบบหลายชั้นอย่างเป็นทางการจาก Google

สรุป

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

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

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

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

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

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

ชาเลนจ์ประจำวัน

คุณหาบั๊กใน Android เจอไหม

โค้ดจริงหนึ่งชิ้น บั๊กที่ซ่อนอยู่หนึ่งจุด วันละหนึ่งครั้ง ลองได้โดยไม่ต้องมีบัญชี

Anthony Fillion-Maillet

เขียนโดย

Anthony Fillion-Maillet

ผู้ก่อตั้ง SharpSkill

เป็นนักพัฒนาฟูลสแตกมากว่า 10 ปี ดูแล SharpSkill และรับผิดชอบทุกสิ่งที่เผยแพร่ที่นี่

อัปเดตเมื่อ 19 สิงหาคม 2569

แท็ก

#android
#mvvm
#mvi
#architecture
#jetpack compose

แชร์

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