Kiến trúc MVVM cho ứng dụng di động: tách giao diện khỏi logic

  • author-image

    Nguyễn Đình Cường

  • blog-tag
  • blog-comment 0 comment
  • created-date 09 Nov, 2026
blog-thumbnail

Ứng dụng nhỏ viết mọi thứ trong một màn hình vẫn chạy được. Nhưng khi tính năng nhiều lên, mã trộn lẫn giao diện, gọi mạng và xử lý dữ liệu trở nên rối và khó kiểm thử. MVVM là một cách tổ chức mã phổ biến để giải quyết vấn đề đó.

MVVM là gì?

MVVM gồm ba phần:

  • Model: dữ liệu và quy tắc nghiệp vụ, kể cả cách lấy dữ liệu (cơ sở dữ liệu, mạng).
  • View: giao diện hiển thị và nhận thao tác của người dùng. Càng ít logic càng tốt.
  • ViewModel: cầu nối; giữ trạng thái của màn hình và xử lý hành động từ View, không biết gì về chi tiết giao diện.

View quan sát trạng thái do ViewModel công bố và tự cập nhật khi nó đổi. Khi người dùng thao tác, View chuyển sự kiện cho ViewModel xử lý.

Vì sao nên dùng?

  1. Dễ kiểm thử: ViewModel không phụ thuộc vào thành phần giao diện nên có thể kiểm thử bằng unit test thuần.
  2. Sống sót qua thay đổi cấu hình: ví dụ xoay màn hình trên Android, trạng thái trong ViewModel không mất.
  3. Tách trách nhiệm: mỗi lớp làm một việc, dễ đọc và dễ bảo trì.
  4. Dễ làm việc nhóm: người làm giao diện và người làm logic ít đụng nhau.

Ví dụ trên Android với Kotlin

Trạng thái màn hình được mô tả bằng một lớp dữ liệu:

data class UsersUiState(
    val loading: Boolean = false,
    val users: List<User> = emptyList(),
    val error: String? = null
)

class UsersViewModel(private val repo: UserRepository) : ViewModel() {

    private val _state = MutableStateFlow(UsersUiState())
    val state: StateFlow<UsersUiState> = _state

    init { load() }

    fun load() {
        viewModelScope.launch {
            _state.value = UsersUiState(loading = true)
            try {
                _state.value = UsersUiState(users = repo.getUsers())
            } catch (e: Exception) {
                _state.value = UsersUiState(error = "Không tải được dữ liệu")
            }
        }
    }
}

View (Compose) chỉ vẽ theo trạng thái:

@Composable
fun UsersScreen(vm: UsersViewModel) {
    val state by vm.state.collectAsState()

    when {
        state.loading -> CircularProgressIndicator()
        state.error != null -> Text(state.error!!)
        else -> LazyColumn {
            items(state.users) { Text(it.name) }
        }
    }
}

Cùng ý tưởng ở nền tảng khác

Trong Flutter, vai trò ViewModel thường do ChangeNotifier, Cubit hay Notifier của Riverpod đảm nhận. Trong SwiftUI, đó là lớp ObservableObject (hoặc macro Observable) cùng thuộc tính được quan sát. React Native dùng custom hook hoặc kho trạng thái để chứa logic. Tên gọi khác nhưng nguyên lý giống nhau: giao diện chỉ phản ánh trạng thái.

Tầng Repository

Thêm một lớp Repository giữa ViewModel và nguồn dữ liệu giúp ViewModel không cần biết dữ liệu đến từ mạng hay cơ sở dữ liệu cục bộ:

interface UserRepository {
    suspend fun getUsers(): List<User>
}

class UserRepositoryImpl(
    private val api: UserApi,
    private val dao: UserDao
) : UserRepository {
    override suspend fun getUsers(): List<User> {
        return try {
            val remote = api.fetchUsers()
            dao.saveAll(remote)
            remote
        } catch (e: IOException) {
            dao.getAll() // dự phòng khi mất mạng
        }
    }
}

Sai lầm thường gặp

  • ViewModel giữ tham chiếu đến View hoặc Context, gây rò rỉ bộ nhớ.
  • Đẩy quá nhiều logic vào View vì "tiện".
  • Một ViewModel khổng lồ cho cả ứng dụng thay vì từng màn hình hoặc tính năng.
  • Thiết kế quá phức tạp cho ứng dụng nhỏ; kiến trúc phải tương xứng với quy mô.
  • Để trạng thái rải rác thành nhiều biến lẻ thay vì một đối tượng trạng thái nhất quán.

Khi nào nên cân nhắc khác?

Có những kiến trúc khác như MVI, Clean Architecture hay Redux-like với luồng dữ liệu một chiều chặt chẽ hơn. MVVM là điểm khởi đầu hợp lý, và nhiều kiến trúc khác xây dựng trên cùng ý tưởng tách giao diện khỏi logic.

Kết luận

MVVM không phải phép màu, nhưng giúp bạn viết ứng dụng có cấu trúc, dễ kiểm thử và dễ mở rộng. Hãy áp dụng ở mức vừa đủ, bắt đầu từ việc đưa trạng thái và logic ra khỏi màn hình.

Cách kiểm thử một ViewModel

Lợi ích lớn của MVVM là kiểm thử dễ. Với ViewModel ở trên, bạn truyền một repository giả trả về dữ liệu cố định hoặc ném lỗi, gọi hàm load, rồi kiểm tra giá trị state có đúng như mong đợi không. Bạn không cần thiết bị hay giao diện nào. Hãy kiểm thử ba tình huống: tải thành công, danh sách rỗng và lỗi mạng. Những bài kiểm thử này chạy trong mili giây và bảo vệ bạn khi sau này thay đổi logic.

Để học kiến trúc ứng dụng qua dự án thực tế, bạn có thể tham khảo các khóa học trên codebem.com.

author_photo
Nguyễn Đình Cường

0 comment