MVVM (Model-View-ViewModel) là một mẫu kiến trúc trong đó ViewModel thay thế Presenter và sử dụng cơ chế phản ứng để giao tiếp với View: ObservableObject trong SwiftUI, LiveData/StateFlow trong Android. ViewModel không có tham chiếu đến View — dữ liệu được truyền qua đăng ký, loại bỏ nhu cầu về giao diện ViewContract và làm cho việc kiểm thử trở nên đơn giản hơn. Apple khuyến nghị MVVM với SwiftUI từ năm 2019, Google khuyến nghị MVVM với Jetpack là kiến trúc chính thức của Android. Tìm hiểu thêm trong Android Architecture Guide.
Những điểm chính
MVVM (Model-View-ViewModel) là một mẫu kiến trúc được John Gossman mô tả vào năm 2005 cho Windows Presentation Foundation (WPF) của Microsoft. ViewModel là thành phần trung tâm chứa trạng thái màn hình và logic nghiệp vụ nhưng không có tham chiếu đến View. Dữ liệu được truyền qua cơ chế ràng buộc phản ứng: View đăng ký thay đổi của ViewModel và tự động kết xuất lại khi dữ liệu thay đổi.
Sự khác biệt chính giữa MVVM và MVP — không có ViewContract. Trong MVP, Presenter gọi các phương thức view.showUser(data), nghĩa là Presenter chủ động "đẩy" dữ liệu vào View. Trong MVVM, View tự "kéo" dữ liệu từ ViewModel thông qua đăng ký: ViewModel không biết mình có người đăng ký hay không. Điều này loại bỏ vấn đề View bị ngắt kết nối — nếu Activity bị hủy khi xoay, ViewModel tiếp tục hoạt động và Activity mới chỉ cần đăng ký dữ liệu hiện tại. Tại IT Sectr, chúng tôi đã sử dụng MVVM trong tất cả các dự án mới từ năm 2020 — mã trở nên dễ dự đoán hơn, kiểm thử ổn định hơn.
| Thành phần | Trách nhiệm | Nền tảng |
|---|---|---|
| Model | Dữ liệu, logic nghiệp vụ, kho lưu trữ | Android/iOS |
| View | Hiển thị, đăng ký ViewModel | Activity/Composable, UIView/SwiftUI View |
| ViewModel | Trạng thái màn hình, logic, điều hướng | ViewModel (Jetpack), ObservableObject |
Ràng buộc phản ứng — nền tảng của MVVM. Trong Android, LiveData (một phần của Jetpack) là một bộ chứa dữ liệu có thể quan sát. Activity đăng ký qua observe(): viewModel.user.observe(this) { user -> binding.name.text = user.name }. Khi user thay đổi, tất cả người đăng ký nhận được giá trị mới tự động. Trong iOS, SwiftUI sử dụng các thuộc tính @Published trong ViewModel — các thay đổi tự động kết xuất lại View. Điều này loại bỏ các lệnh gọi showUser/hideLoading thủ công cần thiết trong MVP.
ViewModel từ Jetpack — thành phần chính thức của Google để triển khai MVVM. ViewModel sống sót qua xoay màn hình: khi cấu hình thay đổi, Activity bị hủy và tạo lại, trong khi ViewModel vẫn ở trong bộ nhớ. Phiên bản Activity mới nhận được cùng một ViewModel thông qua ViewModelProvider. ViewModel không có tham chiếu đến Activity, Context hay View — nó sạch và có thể kiểm thử bằng unit test mà không cần Robolectric.
// ViewModel với StateFlow — triển khai MVVM hiện đại
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) đăng ký 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 -> /* hiển thị trạng thái đang tải */
is UserState.Success -> /* hiển thị dữ liệu */
is UserState.Error -> /* hiển thị lỗi */
}
}.launchIn(lifecycleScope)
viewModel.loadUser(42)
}
}
LiveData vs StateFlow — LiveData (2017) — thành phần phản ứng đầu tiên của Jetpack, được tối ưu hóa cho vòng đời Activity: tự động hủy đăng ký tại onStop. StateFlow (2021) — triển khai Kotlin Flow, không gắn với vòng đời, nhưng yêu cầu hủy đăng ký thủ công qua lifecycleScope. StateFlow hỗ trợ coroutines, concat, map và các toán tử Flow khác mà LiveData không có. Tại IT Sectr, chúng tôi sử dụng StateFlow cho tất cả ViewModel mới — nó ngắn hơn, mạnh hơn và tích hợp tốt hơn với coroutines.
DataBinding và ViewBinding — DataBinding liên kết ViewModel với XML qua @{viewModel.user.name} trực tiếp trong bố cục, loại bỏ mã trong Activity. ViewBinding tạo một lớp an toàn về kiểu để truy cập các view. Google khuyến nghị ViewBinding cho các dự án đơn giản và DataBinding cho các dự án có ràng buộc dữ liệu phức tạp. Trong Jetpack Compose, DataBinding không cần thiết — các hàm @Composable tự động kết xuất lại khi State thay đổi.
MVVM trong iOS được triển khai thông qua ObservableObject từ Combine. ViewModel là một lớp kế thừa ObservableObject, với các thuộc tính @Published. SwiftUI View đăng ký ViewModel qua @ObservedObject hoặc @StateObject. Khi một thuộc tính @Published thay đổi, SwiftUI tự động kết xuất lại View phụ thuộc vào thuộc tính này. Apple giới thiệu SwiftUI vào năm 2019 tại WWDC cùng với Combine — từ đó MVVM trở thành mẫu được khuyến nghị chính thức cho iOS.
import SwiftUI
import Combine
// ViewModel — ObservableObject với các trường @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 — đăng ký 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 tạo ViewModel và quản lý vòng đời của nó (một lần trong suốt vòng đời của View). @ObservedObject — ViewModel được tạo bên ngoài và truyền vào View. WWDC 2022 khuyến nghị @StateObject để tạo và @ObservedObject để truyền ViewModel giữa các Views. Trong iOS 17 (2023), macro @Observable xuất hiện — tự động hóa đăng ký và loại bỏ các chú thích @Published. @Observable là sự tiến hóa của Combine, đưa phát triển iOS đến gần hơn với khả năng phản ứng của Kotlin Flow.
UIKit + MVVM — cho các dự án UIKit (không có SwiftUI), MVVM được triển khai thông qua Combine và @Published với đăng ký trong UIViewController qua sink(). ViewModel giống nhau, View là UIViewController với các đăng ký đến @Published. Combine có sẵn từ iOS 13 (2019) và được tích hợp trong hệ thống — không yêu cầu phụ thuộc bổ sung. Theo Apple Developer Survey (2025), 45% dự án iOS sử dụng Combine ngay cả với UIKit, 35% sử dụng SwiftUI + Combine, 20% sử dụng RxSwift (kế thừa).
MVVM thắng MVP ở ba khía cạnh chính: không có giao diện ViewContract, quản lý đăng ký tự động và sống sót qua xoay màn hình. Trong MVP, mỗi màn hình yêu cầu một giao diện ViewContract + lớp Presenter + đăng ký/hủy đăng ký trong onStart/onStop. Trong MVVM, chỉ ViewModel được tạo — đăng ký trong Activity được thực hiện qua observe() mà không cần detach() thủ công.
| Tiêu chí | MVP | MVVM |
|---|---|---|
| Giao diện ViewContract | 1 mỗi màn hình | Không cần |
| Quản lý đăng ký | attach/detach thủ công | Tự động (lifecycle-aware) |
| Xoay màn hình | Retain-fragment | ViewModel sống sót qua xoay |
| Kiểm thử | Mock ViewContract | Lớp sạch không phụ thuộc |
| Khả năng phản ứng | Callback trong Presenter | LiveData/StateFlow/Combine |
Nhược điểm của MVVM — độ phức tạp khi gỡ lỗi chuỗi phản ứng và rủi ro rò rỉ bộ nhớ khi đăng ký không đúng. LiveData giải quyết vấn đề an toàn vòng đời, StateFlow yêu cầu lifecycleScope, Combine yêu cầu sink với AnyCancellable. Trong MVP, tất cả các lệnh gọi đều rõ ràng (view.showUser), trong MVVM dữ liệu đến qua luồng phản ứng — việc theo dõi yêu cầu điểm dừng gỡ lỗi trong các closure subscribe. Trong ViewModel lớn với nhiều StateFlow, bạn có thể bỏ lỡ cập nhật UI nếu View không đăng ký một Flow cụ thể.
Khi MVP vẫn tốt hơn — trong các dự án có phiên bản Android tối thiểu dưới API 21 (Android 5), nơi Jetpack ViewModel không khả dụng nếu không có AndroidX, và trong các dự án sử dụng UIKit thuần không có Combine (iOS 12 trở xuống). Đối với các dự án kế thừa mà toàn bộ mã nguồn đã ở trên MVP, việc chuyển đổi hoàn toàn sang MVVM không phải lúc nào cũng hợp lý — việc duy trì MVP với việc trích xuất dần logic vào các dịch vụ rẻ hơn là viết lại 100 màn hình trong 3 tháng.
ViewModel được kiểm thử bằng unit test mà không có phụ thuộc nền tảng — đây là lập luận chính ủng hộ MVVM. Trên Android, ViewModel không chứa Activity, Context hay View — tất cả các phụ thuộc (Repository, UseCase) được truyền qua hàm tạo và thay thế bằng các đối tượng mock. Trên iOS, ObservableObject được kiểm thử qua XCTest mà không cần khởi chạy ứng dụng, mang lại sự ổn định và tốc độ thực thi kiểm thử.
// Unit test ViewModel Android với 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)
}
}
ViewModel iOS được kiểm thử tương tự: inject mock UserService, gọi loadUser, kiểm tra trạng thái qua XCTestExpectation. Combine Publisher được kiểm thử qua XCTestCase với wait(for: expectations, timeout: 1.0). Cấu trúc UserState — enum với các giá trị liên kết — cho phép kiểm tra trạng thái chính xác của màn hình sau một thao tác.
Độ phủ mã trong các dự án IT Sectr sử dụng MVVM là 75–90% cho ViewModel và Repository. ViewModel được phủ bởi unit test, Repository bởi integration test với cơ sở dữ liệu thử nghiệm. View trong SwiftUI và Jetpack Compose được kiểm thử bằng UI test (XCUITest, Compose Test) cho các kịch bản quan trọng. Phần còn lại của UI được kiểm tra bằng screenshot test (Snapshot Testing) — nhanh hơn UI test và cung cấp 95% độ tin cậy về hiển thị chính xác.
Câu hỏi thường gặp
Trong MVVM, ViewModel không có tham chiếu đến View — dữ liệu được truyền qua cơ chế phản ứng (LiveData, StateFlow, @Published). Trong MVP, Presenter gọi trực tiếp các phương thức của View qua giao diện ViewContract. MVVM loại bỏ ViewContract và attach/detach thủ công, nhưng yêu cầu hiểu biết về luồng phản ứng. ViewModel sống sót qua xoay màn hình trên Android, Presenter yêu cầu retain-fragment.
Bộ tối thiểu: lifecycle-viewmodel-ktx (ViewModel), lifecycle-livedata-ktx hoặc kotlinx-coroutines-core (StateFlow). Để tiêm phụ thuộc — Hilt hoặc Koin. Cho các thao tác bất đồng bộ — Kotlin Coroutines. Cho ràng buộc dữ liệu phức tạp — DataBinding. Trong Jetpack Compose (được Google khuyến nghị từ 2022), compose-runtime và lifecycle-viewmodel-compose là đủ.
SwiftUI (2019) được thiết kế cho kiến trúc phản ứng: @State và @Published tự động kết xuất lại View khi dữ liệu thay đổi. MVVM là sự phù hợp tự nhiên cho SwiftUI: View — @ViewBuilder, ViewModel — ObservableObject. Apple không áp đặt MVVM là mẫu duy nhất, nhưng tất cả tài liệu đào tạo từ 2019 đều sử dụng ViewModel + SwiftUI. Cho UIKit, Apple khuyến nghị MVC hoặc Coordinator.
Android: viewModelScope tự động hủy coroutines khi ViewModel được dọn dẹp. iOS: AnyCancellable từ Combine tự động hủy đăng ký khi đối tượng chứa nó được giải phóng. SwiftUI @StateObject quản lý vòng đời tự động. Nguyên tắc chính: không lưu trữ tham chiếu đến View/Context trong ViewModel, hủy các thao tác chạy dài khi dọn dẹp, sử dụng weak self trong closures.
StateFlow là lựa chọn hiện đại. LiveData đơn giản hơn và an toàn vòng đời, nhưng StateFlow mạnh hơn: hoạt động với coroutines, hỗ trợ flatMap, combine, filter, không yêu cầu chú thích @Nullable. Kịch bản duy nhất LiveData được ưu tiên — làm việc với mã Java nơi StateFlow (Kotlin Flow API) không khả dụng. Google khuyến nghị StateFlow cho các dự án Kotlin mới.
Tóm tắt
Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay
IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.
Đọc thêm