viewModelScope: nó là gì, liên kết với ViewModel và hoạt động trong Android

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

viewModelScope là một CoroutineScope tích hợp từ thư viện androidx.lifecycle, được liên kết với vòng đời của ViewModel và tự động bị hủy khi ViewModel được dọn dẹp. Theo Google Android Developers, 2025, viewModelScope là cơ chế tiêu chuẩn để chạy coroutine trong kiến trúc MVVM, đảm bảo các thao tác bất đồng bộ an toàn mà không có rủi ro rò rỉ bộ nhớ. ViewModelScope sử dụng Dispatchers.Main theo mặc định và tất cả các thao tác IO bên trong nó phải được thực hiện qua withContext.

Những điểm chính

  • viewModelScope — CoroutineScope từ lifecycle-viewmodel-ktx, bị hủy khi ViewModel.onCleared() được gọi
  • Dispatchers.Main — bộ điều phối mặc định, do đó cập nhật UI bên trong coroutine an toàn
  • onCleared — callback kích hoạt tự động hủy tất cả coroutine đang hoạt động trong viewModelScope
  • clear() vs onCleared() — clear() được framework gọi trước onCleared, đảm bảo hủy scope
  • launch — cách chính để chạy coroutine trong viewModelScope cho các thao tác fire-and-forget

viewModelScope trong Android là gì?

viewModelScope là một thuộc tính mở rộng trên interface ViewModel, được thêm vào thư viện lifecycle-viewmodel-ktx (bắt đầu từ phiên bản 2.1.0). Nó cung cấp một CoroutineScope sẵn sàng sử dụng, được liên kết với vòng đời của ViewModel.

kotlin
// Internal structure (simplified)
val ViewModel.viewModelScope: CoroutineScope
    get() {
        val scope = this.getTag(JOB_KEY)
        if (scope != null) return scope
        return CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
            .also { setTag(JOB_KEY, it) }
    }

Scope được tạo một cách lười biếng (lazy) khi truy cập lần đầu và được lưu vào bộ nhớ đệm qua setTag. Nó sử dụng SupervisorJob, có nghĩa là một ngoại lệ trong một coroutine con sẽ không hủy các coroutine khác. Bộ điều phối mặc định là Dispatchers.Main.immediate, thực thi mã trên luồng chính mà không cần điều phối thêm nếu đã ở trên Main.

Cách viewModelScope nhận thông báo dọn dẹp

Khi ViewModel rời khỏi vòng đời (Activity kết thúc hoặc Fragment bị xóa), hệ thống gọi clear(), kích hoạt onCleared(). Trong callback này, viewModelScope hủy Job của nó, kết thúc đệ quy tất cả coroutine đang hoạt động. Cơ chế được triển khai thông qua interface Closeable, nơi Job của scope được đăng ký như một tài nguyên để tự động đóng.

Cách viewModelScope hoạt động: liên kết với vòng đời ViewModel

Cơ chế liên kết viewModelScope với vòng đời ViewModel dựa trên việc gắn thẻ (tagging) và callback onCleared. Hãy xem xét từng bước.

Bước 1: Tạo scope khi truy cập lần đầu

Khi ViewModel thực thi viewModelScope.launch { ... }, getter kiểm tra xem đã có scope nào được lưu dưới thẻ JOB_KEY chưa. Nếu chưa có scope, một phiên bản CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) mới được tạo. Scope được lưu trữ bên trong ViewModel thông qua một bản đồ thẻ nội bộ.

Bước 2: Vòng đời coroutine

Tất cả coroutine được chạy qua viewModelScope.launch hoặc viewModelScope.async trở thành con của SupervisorJob của scope. Chúng chạy trên luồng chính (trừ khi một bộ điều phối khác được chỉ định qua withContext). Miễn là ViewModel còn sống, coroutine có thể hoạt động, bị tạm dừng hoặc hoàn thành.

Bước 3: Hủy tại onCleared

Khi hệ thống hủy ViewModel, ViewModel.clear() được gọi. Bên trong clear(), những điều sau xảy ra:

  • onCleared() được gọi cho logic dọn dẹp tùy chỉnh
  • Tất cả tài nguyên Closeable đã đăng ký qua addCloseable được đóng
  • Job của viewModelScope chuyển sang trạng thái Cancelled
  • Tất cả coroutine con bị hủy đệ quy
  • Các tham chiếu đến scope được giải phóng cho bộ thu gom rác

Khả năng chống xoay màn hình

Khi màn hình xoay, Activity được tạo lại, nhưng ViewModel vẫn tồn tại (nhờ ViewModelStoreOwner). Điều này có nghĩa là viewModelScope vẫn hoạt động và coroutine tiếp tục thực thi mà không bị gián đoạn. Sau khi Activity được tạo lại, cùng một ViewModel (và cùng một scope) được sử dụng lại — việc tải dữ liệu không bắt đầu lại từ đầu.

viewModelScope trong kiến trúc MVVM

MVVM (Model-View-ViewModel) là kiến trúc được Google khuyến nghị cho các ứng dụng Android. viewModelScope chiếm vị trí trung tâm trong đó với tư cách là người thực thi các thao tác bất đồng bộ.

Vai trò của viewModelScope trong các lớp kiến trúc

LớpThành phầnVai trò của viewModelScope
UIActivity / FragmentQuan sát StateFlow/LiveData từ ViewModel
ViewModelViewModelChạy coroutine qua viewModelScope, quản lý trạng thái UI
RepositoryRepositoryCung cấp các hàm suspend được gọi từ coroutine của viewModelScope
DataDAO / ApiThực thi các yêu cầu thực tế (Room, Retrofit)

ViewModel chạy coroutine qua viewModelScope, bên trong nó gọi các hàm suspend của Repository. Kết quả được chuyển đổi thành StateFlow, được quan sát bởi lớp UI. Thiết kế này đảm bảo phân chia trách nhiệm rõ ràng và khả năng kiểm thử độc lập của từng lớp.

Tại sao viewModelScope trong ViewModel mà không phải trong Fragment

Nếu coroutine được chạy từ Fragment, chúng sẽ bị hủy khi xoay màn hình cùng với việc Fragment bị hủy. ViewModel tồn tại sau khi xoay, do đó coroutine được chạy trong scope của nó tiếp tục thực thi. Đây là lợi thế chính của viewModelScope so với lifecycleScope khi tải dữ liệu.

Ví dụ sử dụng viewModelScope

Hãy xem xét ba kịch bản thực tế khi sử dụng viewModelScope trong ứng dụng Android với Kotlin.

Ví dụ 1: Tải dữ liệu khi tạo ViewModel

kotlin
class ProfileViewModel(
    private val repo: ProfileRepository
) : ViewModel() {

    private val _profile = MutableStateFlow<Profile?>(null)
    val profile: StateFlow<Profile?> = _profile

    init {
        loadProfile()
    }

    private fun loadProfile() {
        viewModelScope.launch {
            val result = repo.getProfile()
            _profile.value = result
        }
    }
}

Trong khối init, việc tải hồ sơ bắt đầu ngay lập tức. Coroutine chạy trên luồng chính (theo mặc định). Kho lưu trữ sử dụng withContext(Dispatchers.IO) cho yêu cầu mạng bên trong hàm suspend của nó, do đó ViewModel không phải lo lắng về việc chuyển đổi luồng.

Ví dụ 2: Xử lý lỗi qua sealed class

kotlin
sealed class UiState {
    object Loading : UiState()
    data class Success(val data: List<Item>) : UiState()
    data class Error(val message: String) : UiState()
}
kotlin
fun fetchItems() {
    _state.value = UiState.Loading
    viewModelScope.launch {
        try {
            val items = repo.getItems()
            _state.value = UiState.Success(items)
        } catch (e: Exception) {
            _state.value = UiState.Error(e.message ?: "Unknown error")
        }
    }
}

Trạng thái UI được mô tả qua sealed class UiState. ViewModel cập nhật trạng thái mỗi khi có thay đổi. Fragment đăng ký StateFlow và chỉ phản ứng với trạng thái hiện tại, bỏ qua các lệnh gọi cũ từ các lần xoay trước đó.

Ví dụ 3: Hủy coroutine trước đó khi có yêu cầu mới

kotlin
private var searchJob: Job? = null

fun search(query: String) {
    searchJob?.cancel()
    searchJob = viewModelScope.launch {
        delay(300)
        val results = repo.search(query)
        _searchResults.value = results
    }
}

Mỗi khi có truy vấn tìm kiếm mới, coroutine trước đó bị hủy. delay(300) thực hiện debounce — tìm kiếm chỉ được thực thi sau 300 ms không hoạt động. Điều này giảm tải máy chủ và ngăn chặn kết quả cũ.

viewModelScope vs lifecycleScope: khi nào chọn cái nào

Cả hai scope đều được cung cấp bởi thư viện AndroidX Lifecycle, nhưng được liên kết với các vòng đời khác nhau. Sự lựa chọn phụ thuộc vào loại nhiệm vụ.

So sánh scope

Đặc điểmviewModelScopelifecycleScope
Chủ sở hữuViewModelLifecycleOwner (Activity/Fragment)
Hủy khi xoayKhông (ViewModel tồn tại)Có (Activity được tạo lại)
Bộ điều phối mặc địnhDispatchers.Main.immediateDispatchers.Main.immediate
Có sẵn trongViewModelActivity, Fragment, Service
Trường hợp sử dụng điển hìnhTải dữ liệu, logic nghiệp vụTương tác UI, hoạt ảnh

Khuyến nghị của Google

Google khuyến nghị sử dụng viewModelScope cho tất cả các tác vụ tải và xử lý dữ liệu. lifecycleScope nên được sử dụng cho các thao tác liên kết với một thời điểm cụ thể của vòng đời UI — ví dụ: bắt đầu hoạt ảnh khi màn hình xuất hiện lần đầu hoặc đăng ký cập nhật vị trí sẽ dừng khi rời khỏi màn hình.

Lỗi thường gặp khi làm việc với viewModelScope

Ngay cả trong một API Android được ghi chép đầy đủ, các nhà phát triển vẫn mắc những lỗi điển hình. Hãy xem xét bốn vấn đề phổ biến nhất.

Lỗi 1: Cập nhật UI sau khi hủy scope

Lỗi nguy hiểm nhất là cố gắng cập nhật StateFlow hoặc LiveData sau khi ViewModel đã được dọn dẹp. Mặc dù viewModelScope bị hủy tại onCleared(), một coroutine có thể thực thi mã trước khi việc hủy có hiệu lực. Sử dụng isActive để kiểm tra hoặc dựa vào việc hoàn thành khối catch.

Lỗi 2: Chạy coroutine mà không xem xét SupervisorJob

viewModelScope sử dụng SupervisorJob nội bộ, cách ly lỗi giữa các coroutine. Tuy nhiên, nếu bạn chạy một coroutine với Job() riêng bên trong viewModelScope.launch, coroutine đó trở thành con của SupervisorJob nhưng sẽ không được bảo vệ khỏi việc hủy do lỗi trong các coroutine khác.

Lỗi 3: Quá nhiều coroutine trong một scope

Mặc dù viewModelScope không có giới hạn cứng, hàng nghìn coroutine đang hoạt động có thể làm chậm hệ thống. Đối với danh sách dữ liệu dài, hãy sử dụng Flow với collectLatest thay vì tạo coroutine riêng cho từng mục.

Lỗi 4: Sử dụng GlobalScope thay vì viewModelScope

Nếu vô tình import GlobalScope thay vì viewModelScope, coroutine sẽ không bị hủy khi ViewModel được dọn dẹp. Điều này dẫn đến rò rỉ bộ nhớ và khả năng ứng dụng bị treo. Luôn đảm bảo rằng coroutine được chạy qua viewModelScope, đặc biệt là trong các lớp con của Fragment.

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

Tôi có thể thay đổi bộ điều phối mặc định của viewModelScope không?

Bạn không thể trực tiếp thay đổi bộ điều phối của viewModelScope — nó được mã hóa cứng là Dispatchers.Main.immediate. Tuy nhiên, bên trong coroutine bạn có thể chuyển sang bộ điều phối khác qua withContext. Để thay đổi bộ điều phối trong kiểm thử, hãy sử dụng TestDispatcher qua Rule.

Làm cách nào để truyền viewModelScope vào Repository?

Đừng truyền scope vào Repository — điều này vi phạm nguyên tắc kiến trúc. Repository nên cung cấp các hàm suspend, và ViewModel tự quản lý coroutine qua viewModelScope. Nếu Repository yêu cầu một scope, hãy xem xét lại kiến trúc theo hướng Clean Architecture.

Tại sao viewModelScope sử dụng SupervisorJob?

SupervisorJob đảm bảo rằng một ngoại lệ trong một coroutine (ví dụ: lỗi tải trong một trong nhiều yêu cầu độc lập) không hủy các coroutine khác. Điều này phù hợp với kịch bản ViewModel, nơi các màn hình khác nhau tải dữ liệu độc lập.

viewModelScope có sẵn trong Jetpack Compose không?

Có, viewModelScope có sẵn trong bất kỳ ViewModel nào bất kể loại UI (View System hay Jetpack Compose). Trong Compose, coroutine cũng được chạy qua viewModelScope, trong khi các hiệu ứng UI sử dụng LaunchedEffectrememberCoroutineScope.

Điều gì xảy ra với coroutine khi gọi viewModelScope.cancel()?

Gọi viewModelScope.cancel() sẽ hủy scope ngay lập tức — tất cả coroutine đang hoạt động kết thúc với CancellationException. Nếu viewModelScope.launch được gọi sau đó, một scope mới được tạo tự động khi truy cập getter lần tiếp theo.

Tổng kết

  • viewModelScope — CoroutineScope liên kết với vòng đời ViewModel, tự động hủy tại onCleared()
  • SupervisorJob + Dispatchers.Main — cấu hình nội bộ đảm bảo cách ly lỗi và truy cập UI an toàn
  • Xoay màn hình — ViewModel tồn tại, do đó coroutine trong viewModelScope tiếp tục mà không cần khởi động lại
  • Kiến trúc MVVM — viewModelScope là yếu tố trung tâm cho các thao tác bất đồng bộ trong lớp ViewModel
  • lifecycleScope — thay thế cho các thao tác liên kết với vòng đời Activity/Fragment, không phải ViewModel
  • StateFlow — cách ưa thích để truyền dữ liệu từ coroutine của viewModelScope đến UI qua sealed class
  • GlobalScope nguy hiểm — thay thế viewModelScope bằng GlobalScope dẫn đến rò rỉ bộ nhớ và ứng dụng bị treo

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