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 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.
// 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.
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ế 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.
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ộ.
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.
Khi hệ thống hủy ViewModel, ViewModel.clear() được gọi. Bên trong clear(), những điều sau xảy ra:
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.
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ộ.
| Lớp | Thành phần | Vai trò của viewModelScope |
|---|---|---|
| UI | Activity / Fragment | Quan sát StateFlow/LiveData từ ViewModel |
| ViewModel | ViewModel | Chạy coroutine qua viewModelScope, quản lý trạng thái UI |
| Repository | Repository | Cung cấp các hàm suspend được gọi từ coroutine của viewModelScope |
| Data | DAO / Api | Thự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.
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.
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.
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.
sealed class UiState {
object Loading : UiState()
data class Success(val data: List<Item>) : UiState()
data class Error(val message: String) : UiState()
}
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 đó.
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ũ.
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ụ.
| Đặc điểm | viewModelScope | lifecycleScope |
|---|---|---|
| Chủ sở hữu | ViewModel | LifecycleOwner (Activity/Fragment) |
| Hủy khi xoay | Không (ViewModel tồn tại) | Có (Activity được tạo lại) |
| Bộ điều phối mặc định | Dispatchers.Main.immediate | Dispatchers.Main.immediate |
| Có sẵn trong | ViewModel | Activity, Fragment, Service |
| Trường hợp sử dụng điển hình | Tải dữ liệu, logic nghiệp vụ | Tương tác UI, hoạt ảnh |
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.
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 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.
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.
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.
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
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.
Đừ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.
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.
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 LaunchedEffect và rememberCoroutineScope.
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
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