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

การเลือก 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
// 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 ที่แตกกระจาย:
// 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 ถูกกำหนดประเภทอย่างชัดเจน:
// 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 ครั้งเดียว
ด้วย MVI สามารถบันทึก Intent ทุกรายการและการเปลี่ยนแปลง state ทุกครั้งได้ การจำลองบั๊กกลายเป็นเรื่องง่าย: เพียงเล่นซ้ำลำดับ Intent
MVI กับ Jetpack Compose
MVI โดดเด่นเป็นพิเศษกับ Jetpack Compose เนื่องจากทั้งสองแบ่งปันปรัชญาเดียวกัน: state ที่ไม่สามารถเปลี่ยนแปลงและ UI แบบ declarative ต่อไปนี้คือวิธีเชื่อมต่อ ViewModel กับหน้าจอ Compose:
// Compose screen consuming MVI state
// The connection between ViewModel and UI is elegant and reactive
@Composable
fun UserProfileScreen(
viewModel: UserProfileMviViewModel = hiltViewModel(),
onNavigateBack: () -> Unit
) {
// Collect the single state
val state by viewModel.state.collectAsStateWithLifecycle()
// Handle side effects
LaunchedEffect(Unit) {
viewModel.sideEffect.collect { effect ->
when (effect) {
is UserProfileSideEffect.ShowSuccess -> {
// Show snackbar
}
is UserProfileSideEffect.NavigateTo -> {
// Navigate
}
}
}
}
// Purely declarative UI based on state
UserProfileContent(
state = state,
onIntent = viewModel::onIntent
)
}
@Composable
private fun UserProfileContent(
state: UserProfileState,
onIntent: (UserProfileIntent) -> Unit
) {
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
// Conditional rendering based on the single state
when {
state.isLoading -> {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.CenterHorizontally)
)
}
state.error != null -> {
ErrorMessage(
message = state.error,
onRetry = {
state.user?.id?.let {
onIntent(UserProfileIntent.LoadProfile(it))
}
}
)
}
state.user != null -> {
ProfileCard(
user = state.user,
isEditing = state.isEditing,
onEditClick = { onIntent(UserProfileIntent.EnableEditMode) },
onSaveClick = { name, bio ->
onIntent(UserProfileIntent.SaveProfile(name, bio))
},
onCancelClick = { onIntent(UserProfileIntent.CancelEdit) }
)
}
}
}
}UI กลายเป็น pure function ของ state: ทำนายได้ ทดสอบได้ และไม่มี side effect ที่ซ่อนอยู่
การเปรียบเทียบโดยละเอียด
เมื่อทั้งสองรูปแบบชัดเจนแล้ว ต่อไปนี้คือการเปรียบเทียบตามเกณฑ์ที่สำคัญจริงๆ ในสภาพแวดล้อมการผลิต
การจัดการ State
ความแตกต่างพื้นฐานอยู่ที่การจัดการ state ความแตกต่างนี้ส่งผลโดยตรงต่อความสามารถในการบำรุงรักษาในระยะยาว
// 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 มีข้อได้เปรียบสำคัญด้วยความสามารถในการทำนาย
// 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
// 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 เพิ่มความซับซ้อนโดยไม่มีประโยชน์ที่สมดุล
// 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 รับประกันความสอดคล้อง
// 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 ที่เรียบง่ายสำหรับหน้าจอง่าย ต่อไปนี้เป็นรูปแบบที่แนะนำ:
// 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ผู้ก่อตั้ง SharpSkill
เป็นนักพัฒนาฟูลสแตกมากว่า 10 ปี ดูแล SharpSkill และรับผิดชอบทุกสิ่งที่เผยแพร่ที่นี่
อัปเดตเมื่อ 19 สิงหาคม 2569
แท็ก
แชร์
บทความที่เกี่ยวข้อง

Jetpack Compose: แอนิเมชันขั้นสูงทีละขั้นตอน
คู่มือฉบับสมบูรณ์เกี่ยวกับแอนิเมชันขั้นสูงใน Compose: ทรานซิชัน AnimatedVisibility, Animatable, ท่าทาง และประสิทธิภาพสำหรับอินเทอร์เฟซ Android ที่ลื่นไหล

20 คำถามสัมภาษณ์ Jetpack Compose ยอดนิยมประจำปี 2026
20 คำถามสัมภาษณ์ Jetpack Compose ที่พบบ่อยที่สุด: recomposition, การจัดการ state, navigation, ประสิทธิภาพ และ pattern สถาปัตยกรรม

Kotlin Flow vs StateFlow vs SharedFlow: คำถามสัมภาษณ์ Android ปี 2026
คำถาม Kotlin Flow vs StateFlow vs SharedFlow ที่ผู้สัมภาษณ์ Android ถามในปี 2026 พร้อมคำตอบชัดเจน ตารางเปรียบเทียบ และโค้ดที่พร้อมใช้งานจริง