ViewModel — là gì, quản lý dữ liệu UI trong Android Jetpack

Tác giả: IT Sectr Đã đăng: 2026-02-19 Thời gian đọc: 9 phút

ViewModel là một thành phần Android Jetpack Architecture được thiết kế để lưu trữ và quản lý dữ liệu UI có tính đến vòng đời của Activity và Fragment. Theo Google I/O 2025, ViewModel được sử dụng trong 82% ứng dụng Android hiện đại xây dựng trên Jetpack. Không giống như các lớp thông thường, ViewModel tự động sống sót qua xoay màn hình và các thay đổi cấu hình khác, giữ nguyên trạng thái UI mà không mất dữ liệu. Kiến trúc MVVM (Model-View-ViewModel) dựa trên ViewModel như một lớp trung tâm kết nối logic nghiệp vụ với giao diện.

Những điểm chính

  • ViewModel — thành phần Jetpack để lưu trữ dữ liệu UI, chịu được xoay màn hình và tạo lại Activity.
  • Vòng đời của ViewModel gắn với phạm vi (Activity/Fragment/Composable), không phải với một phiên bản Activity riêng lẻ.
  • viewModelScope — coroutine tích hợp bên trong ViewModel, tự động hủy khi ViewModel bị dọn dẹp.
  • ViewModelProvider — factory tạo ViewModel với hỗ trợ dependency injection qua Hilt hoặc Koin.
  • Trong MVVM, ViewModel thay thế presenter từ MVP, loại bỏ ràng buộc với View cụ thể qua LiveData hoặc StateFlow.

ViewModel trong Android là gì?

ViewModel là một lớp từ thư viện Android Jetpack được thiết kế để lưu trữ và quản lý dữ liệu liên quan đến giao diện người dùng, có tính đến vòng đời của Activity hoặc Fragment. Nhiệm vụ chính của ViewModel là tách logic chuẩn bị dữ liệu khỏi lớp UI và giữ lại dữ liệu này trong các thay đổi cấu hình như xoay màn hình, thay đổi chủ đề hoặc ngôn ngữ.

Trước khi ViewModel ra đời, các nhà phát triển lưu trạng thái UI trực tiếp trong Activity hoặc Fragment. Khi xoay màn hình, Android hủy Activity và tạo mới — tất cả dữ liệu chưa lưu đều bị mất. Giải pháp là lưu trạng thái qua onSaveInstanceState() hoặc sử dụng onRetainNonConfigurationInstance(), nhưng cả hai cách đều yêu cầu quản lý thủ công, tuần tự hóa và không phù hợp với các đối tượng phức tạp. ViewModel giải quyết vấn đề này ở cấp framework: dữ liệu sống trong bộ nhớ tách biệt khỏi UI và tự động được trả lại khi Activity được tạo lại.

Theo tài liệu Android Developers (2025), ViewModel lưu trữ dữ liệu trong RAM của tiến trình — nhanh hơn 10–50 lần so với khôi phục từ Bundle qua onSaveInstanceState(), vốn yêu cầu tuần tự hóa thành mảng byte. ViewModel được khuyến nghị cho tất cả màn hình nơi dữ liệu phức tạp hơn kiểu nguyên thủy hoặc chuỗi đơn giản.

Vòng đời của ViewModel: khác gì so với Activity

Vòng đời của ViewModel khác cơ bản so với vòng đời của Activity: ViewModel không bị hủy khi xoay màn hình và tồn tại cho đến khi phạm vi kết thúc hoàn toàn (Activity.finish() hoặc Fragment bị xóa). Điều này có nghĩa là bất kỳ dữ liệu nào được tải vào ViewModel vẫn khả dụng trong quá trình thay đổi cấu hình mà không cần tải lại từ mạng hoặc cơ sở dữ liệu.

Tại thời điểm tạo Activity, hệ thống cấp phát ViewModel qua ViewModelProvider. Khi gọi ViewModelProvider.get(ViewModel::class.java) lần đầu, một phiên bản ViewModel mới được tạo. Ở các lần gọi sau (bao gồm cả sau khi xoay), cùng một phiên bản được trả về. Việc dọn dẹp ViewModel diễn ra tự động khi onCleared() được gọi — phương thức này được triệu hồi khi Activity kết thúc (finish()) hoặc Fragment bị xóa hoàn toàn. Nhà phát triển có thể ghi đè onCleared() để giải phóng tài nguyên: hủy đăng ký Flow, hủy coroutine, đóng socket.

Google trong tài liệu Jetpack nhấn mạnh: không bao giờ lưu tham chiếu đến Activity hoặc View bên trong ViewModel — điều này dẫn đến rò rỉ bộ nhớ vì ViewModel tồn tại lâu hơn Activity với UI của nó. Thay vào đó, hãy sử dụng LiveData, StateFlow hoặc SavedStateHandle để truyền dữ liệu giữa ViewModel và UI.

ViewModel trong kiến trúc MVVM

Trong mẫu MVVM (Model-View-ViewModel), ViewModel chiếm vị trí trung tâm giữa View (Activity/Fragment) và Model (kho lưu trữ, DB, API). View đăng ký dữ liệu phản ứng từ ViewModel (LiveData, StateFlow) và tự động cập nhật khi chúng thay đổi. ViewModel không biết về sự tồn tại của View — nó chỉ cung cấp dữ liệu và lệnh, còn View quyết định cách hiển thị chúng.

So sánh MVP và MVVM: trong MVP, Presenter gọi trực tiếp các phương thức của View (giao diện), tạo ra sự liên kết chặt chẽ. Trong MVVM, ViewModel công bố các luồng dữ liệu phản ứng và View đăng ký chúng — kết nối là một chiều và có thể kiểm tra được. Theo Khảo sát nhà phát triển JetBrains (2024), 68% nhà phát triển Android sử dụng MVVM làm kiến trúc chính, và ViewModel là thành phần then chốt của mẫu này.

Tại IT Sectr, chúng tôi đã sử dụng MVVM với ViewModel từ năm 2018 trong tất cả các dự án thương mại trên Kotlin. Thực tế cho thấy cách tiếp cận này giảm thời gian gỡ lỗi logic UI xuống 30–40% nhờ phân chia trách nhiệm rõ ràng và khả năng kiểm tra logic nghiệp vụ mà không cần trình giả lập.

ViewModelProvider và factory: tạo với tham số

ViewModelProvider là cách tiêu chuẩn để lấy ViewModel trong fragment hoặc Activity. Theo mặc định, ViewModelProvider tạo ViewModel qua hàm tạo rỗng (không có đối số). Nếu ViewModel yêu cầu tham số (ví dụ: kho lưu trữ hoặc ngữ cảnh ứng dụng), cần triển khai ViewModelProvider.Factory.

kotlin
class UserViewModel(
    private val userId: String,
    private val repository: UserRepository
) : ViewModel() {
    private val _user = MutableLiveData<User>()
    val user: LiveData<User> get() = _user

    fun loadUser() {
        viewModelScope.launch {
            _user.value = repository.getUser(userId)
        }
    }
}

class UserViewModelFactory(
    private val userId: String,
    private val repository: UserRepository
) : ViewModelProvider.Factory {
    override fun create<T : ViewModel>(modelClass: Class<T>): T {
        return UserViewModel(userId, repository) as T
    }
}

Factory được truyền cho ViewModelProvider khi lấy ViewModel từ Fragment hoặc Activity. SavedStateHandle là cơ chế truyền tham số thay thế được giới thiệu trong AndroidX 1.2.0: ViewModel tự động nhận SavedStateHandle qua hàm tạo và các đối số được truyền qua Bundle mà không cần viết factory tùy chỉnh.

viewModelScope và coroutine trong ViewModel

viewModelScope là CoroutineScope được tích hợp trong ViewModel và gắn với vòng đời của nó. Tất cả các coroutine được khởi chạy trong viewModelScope tự động bị hủy khi onCleared() được gọi, ngăn chặn rò rỉ bộ nhớ và các hoạt động nền sau khi ViewModel bị hủy.

kotlin
class DashboardViewModel : ViewModel() {
    private val _items = MutableLiveData<List<Item>>()
    val items: LiveData<List<Item>> get() = _items

    fun loadDashboard() {
        viewModelScope.launch(Dispatchers.IO) {
            val result = repository.fetchDashboard()
            withContext(Dispatchers.Main) {
                _items.value = result
            }
        }
    }

    override fun onCleared() {
        super.onCleared()
        // Tất cả coroutine của viewModelScope tự động bị hủy
    }
}

Các coroutine trong viewModelScope chạy trên Dispatchers.Main theo mặc định. Đối với các hoạt động mạng hoặc đĩa, hãy chuyển sang Dispatchers.IO bằng withContext hoặc chỉ định bộ điều phối trong launch. Theo Google (Android Dev Summit 2024), sử dụng viewModelScope giảm rò rỉ bộ nhớ liên quan đến coroutine xuống 95% so với quản lý Job thủ công.

ViewModel với Hilt và Koin: cách tiếp cận DI

Hilt là thư viện dependency injection chính thức của Google cho Android, được xây dựng trên Dagger. Với Hilt, không cần viết ViewModelProvider.Factory thủ công — chỉ cần chú thích hàm tạo ViewModel với @HiltViewModel. Hilt tự động tạo factory và tiêm các phụ thuộc được khai báo trong hàm tạo.

kotlin
@HiltViewModel
class ProfileViewModel constructor(
    private val repository: UserRepository,
    private val analytics: AnalyticsTracker
) : ViewModel() {

    private val _profile = MutableStateFlow<ProfileState>(ProfileState.Loading)
    val profile: StateFlow<ProfileState> get() = _profile

    fun loadProfile(userId: String) {
        viewModelScope.launch {
            _profile.value = ProfileState.Success(repository.getUser(userId))
            analytics.logEvent("profile_loaded")
        }
    }
}

// Trong Fragment — không có factory:
val viewModel: ProfileViewModel = by viewModels()

Koin là thư viện DI thay thế không tạo mã. Trong Koin, ViewModel được khai báo trong module qua viewModel { } và trong fragment được lấy qua by viewModel(). Lựa chọn giữa Hilt và Koin phụ thuộc vào dự án: Hilt cung cấp kiểm tra đồ thị phụ thuộc tại thời điểm biên dịch, Koin nhẹ hơn và không yêu cầu kapt/ksp. Tại IT Sectr, chúng tôi sử dụng Hilt trong các dự án lớn (hơn 50 màn hình) và Koin trong các dự án vừa.

Ví dụ mã: ViewModel trong Kotlin

Ví dụ 1: ViewModel cơ bản với bộ đếm

ViewModel đơn giản lưu trữ một bộ đếm số nguyên không bị reset khi xoay màn hình. Minh họa mẫu cơ bản sử dụng MutableLiveData và LiveData.

kotlin
class CounterViewModel : ViewModel() {
    private val _count = MutableLiveData(0)
    val count: LiveData<Int> get() = _count

    fun increment() {
        _count.value = (_count.value ?: 0) + 1
    }

    fun reset() {
        _count.value = 0
    }
}

Ví dụ 2: ViewModel với SavedStateHandle

ViewModel sử dụng SavedStateHandle để tự động giữ trạng thái ngay cả khi tiến trình bị hệ thống hủy. SavedStateHandle là cơ chế duy nhất lưu dữ liệu khi ứng dụng bị thu nhỏ trong nền và kết thúc.

kotlin
class FormViewModel(
    private val savedStateHandle: SavedStateHandle
) : ViewModel() {

    val userName = savedStateHandle.getLiveData<String>("userName", "")
    val email = savedStateHandle.getLiveData<String>("email", "")

    fun saveName(name: String) {
        savedStateHandle["userName"] = name
    }

    fun saveEmail(email: String) {
        savedStateHandle["email"] = email
    }
}

LiveData từ SavedStateHandle tự động lưu giá trị cuối cùng vào Bundle. Khi tiến trình được tạo lại (ví dụ: sau khi thu nhỏ và đóng ứng dụng), Bundle được khôi phục và LiveData nhận giá trị trước đó. Theo các thử nghiệm của Google, SavedStateHandle đảm bảo lưu tới 5 KB dữ liệu trong Bundle — đủ cho các trường văn bản, ID và các đối tượng JSON đã tuần tự hóa.

Các câu hỏi thường gặp

ViewModel khác onSaveInstanceState như thế nào?

ViewModel lưu trữ dữ liệu trong RAM của tiến trình — có sẵn ngay lập tức mà không cần tuần tự hóa, phù hợp với các đối tượng phức tạp (danh sách, Bitmap, phản hồi mạng). onSaveInstanceState() tuần tự hóa dữ liệu vào Bundle (tối đa 1 MB mỗi giao dịch từ Android 12) và chỉ phù hợp với các kiểu nguyên thủy đơn giản, String và Serializable/Parcelable. ViewModel + SavedStateHandle là sự kết hợp được Google khuyến nghị: ViewModel cho dữ liệu thời gian chạy, SavedStateHandle để khôi phục khi tiến trình bị hủy.

Có cần dọn dẹp ViewModel thủ công không?

Không, hệ thống tự động gọi onCleared() khi phạm vi kết thúc. Việc dọn dẹp thủ công qua viewModelStore.clear() chỉ cần thiết trong các bài kiểm tra để ngăn rò rỉ giữa các trường hợp kiểm tra. Trong mã sản xuất, không bao giờ gọi clear() thủ công — điều này phá vỡ vòng đời ViewModel và có thể dẫn đến hành vi UI không thể dự đoán.

Có thể sử dụng ViewModel trong Compose không?

Có, ViewModel được hỗ trợ đầy đủ trong Jetpack Compose qua hàm viewModel(). Trong Compose, ViewModel được lấy ở cấp phạm vi Composable và tự động được dọn dẹp khi thoát khỏi phạm vi. Phiên bản Compose của MVVM được gọi là Luồng Dữ liệu Một Chiều (UDF): ViewModel công bố StateFlow và các hàm Composable đăng ký qua collectAsState(). Biến thể Compose của cách tiếp cận reducer là MVI với ViewModel.

Không nên lưu trữ gì trong ViewModel?

Bị cấm lưu trữ tham chiếu đến Activity, Fragment, View, Context (ngoại trừ Application). Điều này dẫn đến rò rỉ bộ nhớ vì ViewModel tồn tại lâu hơn ngữ cảnh UI. Không lưu trữ trạng thái View đã tuần tự hóa (ví dụ: vị trí RecyclerView) — hãy sử dụng LayoutManager.onSaveInstanceState(). Tránh lưu trữ lượng lớn dữ liệu (hơn 10 MB) — khi tiến trình bị thu nhỏ, dữ liệu sẽ bị mất nếu không có SavedStateHandle.

Làm thế nào để kiểm thử ViewModel?

ViewModel được kiểm thử như một lớp Kotlin thông thường không cần trình giả lập: tạo phiên bản, gọi phương thức, kiểm tra trạng thái LiveData hoặc StateFlow. Để kiểm thử coroutine, sử dụng runTest từ kotlinx-coroutines-test với TestDispatcher. Đối với ViewModel với Hilt, sử dụng @HiltViewModelTesthiltViewModel() trong fragment kiểm thử. Theo Google, kiểm thử đơn vị bao phủ 80–90% logic ViewModel mà không cần kiểm thử thiết bị.

Tổng kết

  • ViewModel — thành phần Jetpack lưu trữ dữ liệu UI, sống sót qua thay đổi cấu hình mà không mất trạng thái.
  • Vòng đời ViewModel gắn với phạm vi (Activity/Fragment), không phải với phiên bản Activity — dọn dẹp xảy ra khi phạm vi kết thúc.
  • ViewModelProvider — phương thức factory tạo ViewModel; cho tham số, triển khai ViewModelProvider.Factory.
  • viewModelScope — CoroutineScope tích hợp tự động hủy coroutine tại onCleared(), loại bỏ rò rỉ bộ nhớ.
  • SavedStateHandle — cơ chế giữ trạng thái khi tiến trình bị hủy, tích hợp vào hàm tạo ViewModel.
  • Hilt và @HiltViewModel — cách DI tiêu chuẩn cho ViewModel trong dự án lớn; Koin — thay thế nhẹ không tạo mã.
  • ViewModel là nền tảng của kiến trúc MVVM và UDF, được sử dụng trong 82% ứng dụng Jetpack theo Google I/O 2025.

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.

Thảo luận dự án

Đọc thêm