MVVM (Model-View-ViewModel) เป็นรูปแบบสถาปัตยกรรมที่ ViewModel แทนที่ Presenter และใช้กลไกเชิงปฏิกิริยาเพื่อสื่อสารกับ View: ObservableObject ใน SwiftUI, LiveData/StateFlow ใน Android ViewModel ไม่มีการอ้างอิงถึง View — ข้อมูลถูกส่งผ่านการสมัครสมาชิก ซึ่งช่วยขจัดความจำเป็นต้องใช้อินเทอร์เฟซ ViewContract และทำให้การทดสอบง่ายยิ่งขึ้น Apple แนะนำ MVVM กับ SwiftUI ตั้งแต่ปี 2019 Google แนะนำ MVVM กับ Jetpack เป็นสถาปัตยกรรมอย่างเป็นทางการของ Android เรียนรู้เพิ่มเติมใน Android Architecture Guide
ประเด็นสำคัญ
MVVM (Model-View-ViewModel) เป็นรูปแบบสถาปัตยกรรมที่อธิบายโดย John Gossman ในปี 2005 สำหรับ Windows Presentation Foundation (WPF) ของ Microsoft ViewModel เป็นคอมโพเนนต์ศูนย์กลางที่ประกอบด้วยสถานะหน้าจอและตรรกะทางธุรกิจ แต่ไม่มีการอ้างอิงถึงView ข้อมูลถูกส่งผ่านกลไกการผูกแบบปฏิกิริยา: View สมัครสมาชิกการเปลี่ยนแปลงของ ViewModel และเรนเดอร์ใหม่โดยอัตโนมัติเมื่อข้อมูลเปลี่ยนแปลง
ความแตกต่างหลักระหว่าง MVVM และ MVP — ไม่มี ViewContract ใน MVP, Presenter เรียกเมธอด view.showUser(data) นั่นคือ Presenter "ผลัก" ข้อมูลไปยัง View อย่างจริงจัง ใน MVVM, View เอง "ดึง" ข้อมูลจาก ViewModel ผ่านการสมัครสมาชิก: ViewModel ไม่รู้ว่ามีสมาชิกหรือไม่ สิ่งนี้ช่วยขจัดปัญหา View ที่ถูกตัดการเชื่อมต่อ — หาก Activity ถูกทำลายเมื่อหมุน ViewModel ยังคงทำงานต่อไป และ Activity ใหม่เพียงแค่สมัครสมาชิกข้อมูลปัจจุบัน ที่ IT Sectr เราใช้ MVVM ในโปรเจกต์ใหม่ทั้งหมดตั้งแต่ปี 2020 — โค้ดคาดเดาได้ง่ายขึ้น การทดสอบมีเสถียรภาพมากขึ้น
| คอมโพเนนต์ | ความรับผิดชอบ | แพลตฟอร์ม |
|---|---|---|
| Model | ข้อมูล, ตรรกะทางธุรกิจ, พื้นที่เก็บข้อมูล | Android/iOS |
| View | การแสดงผล, การสมัครสมาชิก ViewModel | Activity/Composable, UIView/SwiftUI View |
| ViewModel | สถานะหน้าจอ, ตรรกะ, การนำทาง | ViewModel (Jetpack), ObservableObject |
การผูกแบบปฏิกิริยา — พื้นฐานของ MVVM ใน Android, LiveData (ส่วนหนึ่งของ Jetpack) เป็นตัวเก็บข้อมูลที่สังเกตได้ Activity สมัครสมาชิกผ่าน observe(): viewModel.user.observe(this) { user -> binding.name.text = user.name } เมื่อ user เปลี่ยนแปลง สมาชิกทั้งหมดได้รับค่าใหม่โดยอัตโนมัติ ใน iOS, SwiftUI ใช้คุณสมบัติ @Published ใน ViewModel — การเปลี่ยนแปลงเรนเดอร์ View ใหม่โดยอัตโนมัติ สิ่งนี้ช่วยขจัดการเรียก showUser/hideLoading ด้วยตนเองที่จำเป็นใน MVP
ViewModel จาก Jetpack — คอมโพเนนต์อย่างเป็นทางการจาก Google สำหรับการนำ MVVM ไปใช้ ViewModel ทนต่อการหมุนหน้าจอ: เมื่อการกำหนดค่าเปลี่ยนแปลง Activity ถูกทำลายและสร้างใหม่ ในขณะที่ ViewModel ยังคงอยู่ในหน่วยความจำ อินสแตนซ์ Activity ใหม่ได้รับ ViewModel เดียวกันผ่าน ViewModelProvider ViewModel ไม่มีการอ้างอิงถึง Activity, Context หรือ View — มันสะอาดและทดสอบได้ด้วย unit test โดยไม่ต้องใช้ Robolectric
// ViewModel กับ StateFlow — การนำ MVVM ที่ทันสมัย
class UserViewModel(
private val repository: UserRepository
) : ViewModel() {
private val _state = MutableStateFlow<UserState>(UserState.Loading)
val state: StateFlow<UserState> = _state.asStateFlow()
fun loadUser(userId: Int) {
viewModelScope.launch {
_state.value = UserState.Loading
repository.getUser(userId)
.onSuccess { user ->
_state.value = UserState.Success(user)
}
.onFailure { e ->
_state.value = UserState.Error(e.message ?: "Unknown")
}
}
}
}
sealed interface UserState {
data object Loading : UserState
data class Success(val user: User) : UserState
data class Error(val message: String) : UserState
}
// View (Activity) สมัครสมาชิก state
class UserActivity : AppCompatActivity() {
private val viewModel: UserViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.state.onEach { state ->
when (state) {
is UserState.Loading -> /* แสดงสถานะกำลังโหลด */
is UserState.Success -> /* แสดงข้อมูล */
is UserState.Error -> /* แสดงข้อผิดพลาด */
}
}.launchIn(lifecycleScope)
viewModel.loadUser(42)
}
}
LiveData vs StateFlow — LiveData (2017) — คอมโพเนนต์เชิงปฏิกิริยาตัวแรกของ Jetpack, ปรับให้เหมาะสมสำหรับวงจรชีวิต Activity: ยกเลิกการสมัครสมาชิกอัตโนมัติที่ onStop StateFlow (2021) — การนำ Kotlin Flow ไปใช้, ไม่ผูกกับวงจรชีวิต, แต่ต้องการการยกเลิกการสมัครสมาชิกด้วยตนเองผ่าน lifecycleScope StateFlow รองรับ coroutines, concat, map และตัวดำเนินการ Flow อื่นๆ ซึ่ง LiveData ไม่มี ที่ IT Sectr เราใช้ StateFlow สำหรับ ViewModel ใหม่ทั้งหมด — สั้นกว่า, ทรงพลังกว่า, และผสานรวมกับ coroutines ได้ดีกว่า
DataBinding และ ViewBinding — DataBinding ผูก ViewModel กับ XML ผ่าน @{viewModel.user.name} โดยตรงในเลย์เอาต์, ยกเลิกโค้ดใน Activity ViewBinding สร้างคลาสที่ปลอดภัยต่อชนิดสำหรับการเข้าถึง View Google แนะนำ ViewBinding สำหรับโปรเจกต์ง่ายๆ และ DataBinding สำหรับโปรเจกต์ที่มีการผูกข้อมูลซับซ้อน ใน Jetpack Compose ไม่จำเป็นต้องใช้ DataBinding — ฟังก์ชัน @Composable เรนเดอร์ใหม่โดยอัตโนมัติเมื่อ State เปลี่ยนแปลง
MVVM ใน iOS ถูกนำไปใช้ผ่าน ObservableObject จาก Combine ViewModel เป็นคลาสที่สืบทอด ObservableObject พร้อมคุณสมบัติ @Published SwiftUI View สมัครสมาชิก ViewModel ผ่าน @ObservedObject หรือ @StateObject เมื่อคุณสมบัติ @Published เปลี่ยนแปลง SwiftUI จะเรนเดอร์ View ที่ขึ้นอยู่กับคุณสมบัตินี้โดยอัตโนมัติ Apple เปิดตัว SwiftUI ในปี 2019 ที่ WWDC พร้อมกับ Combine — ตั้งแต่นั้นมา MVVM ก็กลายเป็นรูปแบบที่แนะนำอย่างเป็นทางการสำหรับ iOS
import SwiftUI
import Combine
// ViewModel — ObservableObject พร้อมฟิลด์ @Published
final class UserViewModel: ObservableObject {
@Published private(set) var state: UserState = .loading
private let service: UserService
init(service: UserService) {
self.service = service
}
func loadUser(id: Int) {
state = .loading
service.fetchUser(id: id) { [weak self] result in
guard let self else { return }
switch result {
case .success(let user):
self.state = .success(user)
case .failure(let error):
self.state = .error(error.localizedDescription)
}
}
}
}
enum UserState {
case loading
case success(User)
case error(String)
}
// SwiftUI View — สมัครสมาชิก ViewModel
struct UserView: View {
@StateObject private var viewModel: UserViewModel
var body: some View {
switch viewModel.state {
case .loading:
ProgressView()
case .success(let user):
VStack {
Text(user.name).font(.title)
Text(user.email).font(.body)
}
case .error(let message):
Text(message).foregroundColor(.red)
}
}
}
@StateObject vs @ObservedObject — @StateObject สร้าง ViewModel และจัดการวงจรชีวิตของมัน (ครั้งเดียวต่ออายุของ View) @ObservedObject — ViewModel ถูกสร้างขึ้นจากภายนอกและส่งผ่านไปยัง View WWDC 2022 แนะนำ @StateObject สำหรับการสร้างและ @ObservedObject สำหรับการส่ง ViewModel ระหว่าง Views ใน iOS 17 (2023), มาโคร @Observable ปรากฏขึ้น — ทำให้การสมัครสมาชิกเป็นอัตโนมัติและยกเลิกคำอธิบายประกอบ @Published @Observable คือวิวัฒนาการของ Combine, นำการพัฒนา iOS เข้าใกล้การตอบสนองของ Kotlin Flow มากขึ้น
UIKit + MVVM — สำหรับโปรเจกต์ UIKit (ไม่มี SwiftUI), MVVM ถูกนำไปใช้ผ่าน Combine และ @Published พร้อมการสมัครสมาชิกใน UIViewController ผ่าน sink() ViewModel เหมือนเดิม, View คือ UIViewController ที่มีการสมัครสมาชิก @Published Combine พร้อมใช้งานตั้งแต่ iOS 13 (2019) และสร้างไว้ในระบบ — ไม่ต้องการ dependencies เพิ่มเติม ตาม Apple Developer Survey (2025), 45% ของโปรเจกต์ iOS ใช้ Combine แม้กับ UIKit, 35% ใช้ SwiftUI + Combine, 20% ใช้ RxSwift (เดิม)
MVVM ชนะ MVP ในสามด้านหลัก: ไม่ต้องมีอินเทอร์เฟซ ViewContract, การจัดการสมัครสมาชิกอัตโนมัติ, และทนต่อการหมุนหน้าจอ ใน MVP, แต่ละหน้าจอต้องใช้อินเทอร์เฟซ ViewContract + คลาส Presenter + การสมัครสมาชิก/ยกเลิกใน onStart/onStop ใน MVVM, สร้างเฉพาะ ViewModel — การสมัครสมาชิกใน Activity ทำผ่าน observe() โดยไม่ต้อง detach() ด้วยตนเอง
| เกณฑ์ | MVP | MVVM |
|---|---|---|
| อินเทอร์เฟซ ViewContract | 1 ต่อหน้าจอ | ไม่จำเป็น |
| การจัดการสมัครสมาชิก | attach/detach ด้วยตนเอง | อัตโนมัติ (lifecycle-aware) |
| การหมุนหน้าจอ | Retain-fragment | ViewModel ทนต่อการหมุน |
| การทดสอบ | Mock ViewContract | คลาสสะอาดไม่มี dependencies |
| การตอบสนอง | Callback ใน Presenter | LiveData/StateFlow/Combine |
ข้อเสียของ MVVM — ความซับซ้อนในการดีบักเชนเชิงปฏิกิริยาและความเสี่ยงของการรั่วไหลของหน่วยความจำเมื่อสมัครสมาชิกไม่ถูกต้อง LiveData แก้ปัญหาความปลอดภัยของวงจรชีวิต, StateFlow ต้องใช้ lifecycleScope, Combine ต้องใช้ sink กับ AnyCancellable ใน MVP, การเรียกทั้งหมดชัดเจน (view.showUser), ใน MVVM ข้อมูลมาผ่านสตรีมเชิงปฏิกิริยา — การติดตามต้องใช้เบรกพอยต์ดีบักใน closures subscribe ใน ViewModel ขนาดใหญ่ที่มี StateFlow หลายตัว, คุณอาจพลาดการอัปเดต UI ถ้า View ไม่ได้สมัครสมาชิก Flow ที่เฉพาะเจาะจง
เมื่อ MVP ยังคงดีกว่า — ในโปรเจกต์ที่มีเวอร์ชัน Android ขั้นต่ำต่ำกว่า API 21 (Android 5), ซึ่ง Jetpack ViewModel ไม่พร้อมใช้งานโดยไม่มี AndroidX, และในโปรเจกต์ที่ใช้ UIKit บริสุทธิ์โดยไม่มี Combine (iOS 12 และต่ำกว่า) สำหรับโปรเจกต์เดิมที่โค้ดเบสทั้งหมดอยู่บน MVP แล้ว, การเปลี่ยนไปใช้ MVVM อย่างสมบูรณ์นั้นไม่สมเหตุสมผลเสมอไป — การบำรุงรักษา MVP ด้วยการแยกตรรกะออกเป็นบริการอย่างค่อยเป็นค่อยไปนั้นถูกกว่าการเขียน 100 หน้าจอใหม่ใน 3 เดือน
ViewModel ทดสอบด้วย unit test โดยไม่มี dependencies ของแพลตฟอร์ม — นี่คือข้อโต้แย้งหลักที่สนับสนุน MVVM บน Android, ViewModel ไม่มี Activity, Context หรือ View — dependencies ทั้งหมด (Repository, UseCase) ถูกส่งผ่านคอนสตรักเตอร์และแทนที่ด้วยอ็อบเจกต์ mock บน iOS, ObservableObject ทดสอบผ่าน XCTest โดยไม่ต้องเปิดแอปพลิเคชัน, ให้ความเสถียรและความเร็วในการดำเนินการทดสอบ
// Unit test ViewModel Android ด้วย MockK
class UserViewModelTest {
private val repository = mockk<UserRepository>()
private val viewModel = UserViewModel(repository)
@Test
fun loadUser_success_updatesState() = runTest {
val user = User(1, "John", "john@test.com")
coEvery { repository.getUser(1) } returns Result.success(user)
viewModel.loadUser(1)
assertEquals(UserState.Success(user), viewModel.state.value)
}
@Test
fun loadUser_error_updatesErrorState() = runTest {
val error = RuntimeException("Network error")
coEvery { repository.getUser(1) } returns Result.failure(error)
viewModel.loadUser(1)
val state = viewModel.state.value
assertTrue(state is UserState.Error)
assertEquals("Network error", (state as UserState.Error).message)
}
}
iOS ViewModel ทดสอบในลักษณะเดียวกัน: ฉีด mock UserService, เรียก loadUser, ตรวจสอบสถานะผ่าน XCTestExpectation Combine Publisher ทดสอบผ่าน XCTestCase ด้วย wait(for: expectations, timeout: 1.0) โครงสร้าง UserState — enum พร้อมค่าที่เกี่ยวข้อง — ช่วยให้ตรวจสอบสถานะหน้าจอที่แน่นอนหลังการดำเนินการ
ความครอบคลุมโค้ด ในโปรเจกต์ IT Sectr ที่ใช้ MVVM อยู่ที่ 75–90% สำหรับ ViewModel และ Repository ViewModel ครอบคลุมโดย unit test, Repository ครอบคลุมโดย integration test กับฐานข้อมูลทดสอบ View ใน SwiftUI และ Jetpack Compose ทดสอบด้วย UI test (XCUITest, Compose Test) สำหรับสถานการณ์วิกฤต UI ที่เหลือตรวจสอบด้วย screenshot test (Snapshot Testing) — เร็วกว่า UI test และให้ความมั่นใจ 95% ในความถูกต้องของการแสดงผล
คำถามที่พบบ่อย
ใน MVVM, ViewModel ไม่มีการอ้างอิงถึง View — ข้อมูลถูกส่งผ่านกลไกเชิงปฏิกิริยา (LiveData, StateFlow, @Published) ใน MVP, Presenter เรียกเมธอดของ View โดยตรงผ่านอินเทอร์เฟซ ViewContract MVVM ขจัด ViewContract และ attach/detach ด้วยตนเอง แต่ต้องเข้าใจสตรีมเชิงปฏิกิริยา ViewModel ทนต่อการหมุนหน้าจอบน Android, Presenter ต้องใช้ retain-fragment
ชุดขั้นต่ำ: lifecycle-viewmodel-ktx (ViewModel), lifecycle-livedata-ktx หรือ kotlinx-coroutines-core (StateFlow) สำหรับการฉีด — Hilt หรือ Koin สำหรับการดำเนินการแบบอะซิงโครนัส — Kotlin Coroutines สำหรับการผูกข้อมูลที่ซับซ้อน — DataBinding ใน Jetpack Compose (แนะนำโดย Google ตั้งแต่ 2022), compose-runtime และ lifecycle-viewmodel-compose ก็เพียงพอ
SwiftUI (2019) ออกแบบมาสำหรับสถาปัตยกรรมเชิงปฏิกิริยา: @State และ @Published เรนเดอร์ View ใหม่โดยอัตโนมัติเมื่อข้อมูลเปลี่ยนแปลง MVVM เข้ากันได้อย่างเป็นธรรมชาติกับ SwiftUI: View — @ViewBuilder, ViewModel — ObservableObject Apple ไม่บังคับ MVVM เป็นรูปแบบเดียว แต่สื่อการฝึกอบรมทั้งหมดตั้งแต่ 2019 ใช้ ViewModel + SwiftUI สำหรับ UIKit, Apple แนะนำ MVC หรือ Coordinator
Android: viewModelScope ยกเลิก coroutines โดยอัตโนมัติเมื่อล้าง ViewModel iOS: AnyCancellable จาก Combine ยกเลิกการสมัครสมาชิกโดยอัตโนมัติเมื่ออ็อบเจกต์ที่เก็บมันถูกดีโลเคต SwiftUI @StateObject จัดการวงจรชีวิตโดยอัตโนมัติ กฎหลัก: อย่าเก็บการอ้างอิงถึง View/Context ใน ViewModel, ยกเลิกการดำเนินการระยะยาวเมื่อล้าง, ใช้ weak self ใน closures
StateFlow เป็นตัวเลือกที่ทันสมัย LiveData ง่ายกว่าและปลอดภัยต่อวงจรชีวิต แต่ StateFlow ทรงพลังกว่า: ทำงานกับ coroutines, รองรับ flatMap, combine, filter, ไม่ต้องการคำอธิบายประกอบ @Nullable สถานการณ์เดียวที่ LiveData ดีกว่า — การทำงานกับโค้ด Java ที่ StateFlow (Kotlin Flow API) ไม่พร้อมใช้งาน Google แนะนำ StateFlow สำหรับโปรเจกต์ Kotlin ใหม่
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม