viewModelScope — یک CoroutineScope داخلی از کتابخانه androidx.lifecycle است که به چرخه حیات ViewModel متصل شده و هنگام پاکسازی آن به طور خودکار لغو میشود. به گفته Google Android Developers, 2025، viewModelScope مکانیزم استانداردی برای راهاندازی کوروتینها در معماری MVVM است و کار ایمن با عملیاتهای ناهمگام را بدون خطر نشت حافظه فراهم میکند. ViewModelScope به طور پیشفرض از Dispatchers.Main استفاده میکند و تمام عملیاتهای IO درون آن باید از طریق withContext انجام شوند.
نکات اصلی
viewModelScope — یک ویژگی الحاقی (extension property) بر روی رابط ViewModel است که در کتابخانه lifecycle-viewmodel-ktx (از نسخه 2.1.0) اضافه شده است. این ویژگی یک CoroutineScope آماده و متصل به چرخه حیات ViewModel فراهم میکند.
// ساختار داخلی (سادهشده)
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 به صورت تنبل (lazy) در اولین دسترسی ایجاد میشود و از طریق setTag ذخیره میشود. از SupervisorJob استفاده میشود، به این معنی که استثنا در یک کوروتین فرزند، بقیه را لغو نمیکند. توزیعکننده پیشفرض Dispatchers.Main.immediate است که کد را در نخ اصلی بدون توزیعبندی اضافی اجرا میکند، اگر فراخوانی از قبل روی Main باشد.
وقتی ViewModel چرخه حیات را ترک میکند (Activity پایان یافته یا Fragment حذف شده)، سیستم clear() را فراخوانی میکند که onCleared() را فعال میکند. در این فراخوانی viewModelScope Job خود را لغو میکند که به صورت بازگشتی تمام کوروتینهای فعال را خاتمه میدهد. این مکانیزم از طریق رابط Closeable پیادهسازی شده است، جایی که Job scope به عنوان منبعی برای بستن خودکار ثبت میشود.
مکانیزم اتصال viewModelScope به چرخه حیات ViewModel بر اساس برچسبگذاری و فراخوانی onCleared است. بیایید گام به گام نحوه کار آن را بررسی کنیم.
وقتی ViewModel دستور viewModelScope.launch { ... } را اجرا میکند، getter بررسی میکند که آیا scope ذخیرهشده با برچسب JOB_KEY وجود دارد یا خیر. اگر scope هنوز ایجاد نشده باشد — یک نمونه جدید از CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) ایجاد میشود. Scope در داخل ViewModel از طریق یک نقشه داخلی از برچسبها ذخیره میشود.
تمام کوروتینهای راهاندازیشده از طریق viewModelScope.launch یا viewModelScope.async نسبت به SupervisorJob scope فرزند میشوند. آنها روی نخ اصلی کار میکنند (اگر توزیعکننده دیگری از طریق withContext مشخص نشده باشد). تا زمانی که ViewModel زنده است — کوروتینها میتوانند فعال، معلق یا خاتمهیافته باشند.
وقتی سیستم ViewModel را نابود میکند، ViewModel.clear() فراخوانی میشود. درون clear() موارد زیر رخ میدهد:
هنگام چرخش صفحه، Activity بازآفرینی میشود، اما ViewModel حفظ میشود (به لطف ViewModelStoreOwner). این بدان معناست که viewModelScope فعال میماند و کوروتینها بدون وقفه به کار خود ادامه میدهند. پس از بازآفرینی Activity، همان ViewModel (و همان scope) دوباره استفاده میشود — بارگذاری داده از نو شروع نمیشود.
MVVM (Model-View-ViewModel) — معماری توصیهشده توسط Google برای برنامههای Android است. viewModelScope در این معماری به عنوان مجری عملیاتهای ناهمگام جایگاه مرکزی دارد.
| لایه | کامپوننت | نقش viewModelScope |
|---|---|---|
| UI | Activity / Fragment | StateFlow/LiveData از ViewModel را مشاهده میکند |
| ViewModel | ViewModel | کوروتینها را از طریق viewModelScope راهاندازی میکند، وضعیت UI را مدیریت میکند |
| Repository | Repository | تابعهای suspend فراخوانیشده از کوروتینهای viewModelScope را دریافت میکند |
| Data | DAO / Api | درخواستهای واقعی را اجرا میکند (Room, Retrofit) |
ViewModel از طریق viewModelScope کوروتینهایی را راهاندازی میکند که درون آنها تابعهای suspend Repository را فراخوانی میکند. نتیجه به StateFlow تبدیل میشود که توسط لایه UI مشاهده میشود. این طرح جداسازی واضح مسئولیتها و قابلیت تست هر لایه را به طور مستقل تضمین میکند.
اگر کوروتینها از Fragment راهاندازی میشدند، هنگام چرخش صفحه همراه با نابودی Fragment لغو میشدند. ViewModel از چرخش جان سالم به در میبرد، بنابراین کوروتینهای راهاندازیشده در scope آن به کار خود ادامه میدهند. این مزیت کلیدی viewModelScope نسبت به lifecycleScope هنگام بارگذاری داده است.
سه سناریوی عملی استفاده از viewModelScope در برنامه Android به زبان 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
}
}
}
در بلوک init بلافاصله بارگذاری پروفایل آغاز میشود. کوروتین به طور پیشفرض روی نخ اصلی اجرا میشود. Repository برای درخواست شبکه درون تابع suspend خود از withContext(Dispatchers.IO) استفاده میکند، بنابراین ViewModel نگران جابجایی نخها نیست.
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")
}
}
}
وضعیت UI از طریق sealed class UiState توصیف میشود. ViewModel state را در هر تغییر بهروزرسانی میکند. Fragment روی StateFlow مشترک شده و فقط به وضعیت جاری واکنش نشان میدهد و فراخوانیهای قدیمی در چرخشهای مکرر را نادیده میگیرد.
private var searchJob: Job? = null
fun search(query: String) {
searchJob?.cancel()
searchJob = viewModelScope.launch {
delay(300)
val results = repo.search(query)
_searchResults.value = results
}
}
در هر درخواست جستجوی جدید، کوروتین قبلی لغو میشود. delay(300) debounce را پیادهسازی میکند — جستجو فقط پس از 300 میلیثانیه مکث در ورودی انجام میشود. این کار بار سرور را کاهش میدهد و از نتایج قدیمی جلوگیری میکند.
هر دو scope توسط کتابخانه AndroidX Lifecycle ارائه میشوند، اما به چرخههای حیات متفاوتی متصل هستند. انتخاب بین آنها به نوع وظیفه بستگی دارد.
| ویژگی | viewModelScope | lifecycleScope |
|---|---|---|
| مالک | ViewModel | LifecycleOwner (Activity/Fragment) |
| لغو در چرخش | خیر (ViewModel حفظ میشود) | بله (Activity بازآفرینی میشود) |
| توزیعکننده پیشفرض | Dispatchers.Main.immediate | Dispatchers.Main.immediate |
| در دسترس در | ViewModel | Activity, Fragment, Service |
| سناریوی معمول | بارگذاری داده، منطق کسبوکار | تعامل UI، انیمیشنها، اسنکبارها |
Google استفاده از viewModelScope را برای تمام وظایف مرتبط با بارگذاری و پردازش داده توصیه میکند. lifecycleScope باید برای عملیاتهای متصل به لحظه خاصی از عمر UI استفاده شود — مانند راهاندازی انیمیشن در اولین نمایش صفحه یا اشتراک در بهروزرسانیهای Location که باید با خروج از صفحه متوقف شود.
حتی در API مستند Android، توسعهدهندگان مرتکب خطاهای مشخصی میشوند. چهار مشکل رایج را بررسی میکنیم.
موذیترین خطا — تلاش برای بهروزرسانی StateFlow یا LiveData پس از پاکسازی ViewModel. اگرچه viewModelScope در onCleared() لغو میشود، کوروتین ممکن است کد را تا لحظه لغو واقعی اجرا کند. برای بررسی از isActive استفاده کنید یا به اتمام بلوک catch اعتماد کنید.
viewModelScope در داخل از SupervisorJob استفاده میکند که خطاها را بین کوروتینها ایزوله میکند. اما اگر کوروتینی با Job() خود درون viewModelScope.launch راهاندازی کنید، آن کوروتین فرزند SupervisorJob میشود، اما در برابر لغو هنگام خطا در کوروتینهای دیگر محافظت نخواهد شد.
اگرچه viewModelScope محدودیت سختافزاری ندارد، هزاران کوروتین فعال میتوانند سیستم را کند کنند. برای لیستهای طولانی داده به جای ایجاد کوروتین جداگانه برای هر عنصر از Flow با collectLatest استفاده کنید.
اگر به طور تصادفی به جای viewModelScope از GlobalScope وارد کنید، کوروتین هنگام پاکسازی ViewModel لغو نخواهد شد. این منجر به نشت حافظه و احتمالاً کرش میشود. همیشه بررسی کنید که کوروتینها از طریق viewModelScope راهاندازی میشوند، به ویژه در fragmentهای مشتقشده.
سوالات متداول
تغییر مستقیمتوزیعکننده viewModelScope ممکن نیست — آن به صورت سختافزاری به عنوان Dispatchers.Main.immediate تنظیم شده است. اما درون کوروتین میتوان از طریق withContext بهتوزیعکننده دیگری سوئیچ کرد. برای تغییرتوزیعکننده در تستها از TestDispatcher از طریق Rule استفاده کنید.
scope را به Repository منتقل نکنید — این اصول معماری را نقض میکند. Repository باید تابعهای suspend ارائه دهد و ViewModel خود کوروتینها را از طریق viewModelScope مدیریت کند. اگر Repository نیاز به scope دارد — معماری را به نفع Clean Architecture بازبینی کنید.
SupervisorJob تضمین میکند که استثنا در یک کوروتین (مثلاً خطای بارگذاری یکی از چندین درخواست مستقل) بقیه کوروتینها را لغو نمیکند. این با سناریوی ViewModel مطابقت دارد، جایی که صفحات مختلف دادههای مستقل بارگذاری میکنند.
بله، viewModelScope بدون توجه به نوع UI (View System یا Jetpack Compose) در هر ViewModel در دسترس است. در Compose نیز کوروتینها از طریق viewModelScope راهاندازی میشوند و برای اثرات UI از LaunchedEffect و rememberCoroutineScope استفاده میشود.
فراخوانی viewModelScope.cancel() scope را فوراً لغو میکند — تمام کوروتینهای فعال با CancellationException خاتمه مییابند. اگر پس از آن viewModelScope.launch را فراخوانی کنید، scope جدید در دسترسی بعدی به getter به طور خودکار ایجاد میشود.
خلاصه
ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد
IT Sectr از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.
همچنین بخوانید