viewModelScope هو CoroutineScope مدمج من مكتبة androidx.lifecycle يرتبط بدورة حياة ViewModel ويتم إلغاؤه تلقائياً عند مسحها. وفقاً لـ Google Android Developers, 2025، فإن viewModelScope هو الآلية القياسية لإطلاق الكوروتينات في بنية MVVM، مما يضمن عمليات غير متزامنة آمنة دون خطر تسرب الذاكرة. يستخدم ViewModelScope Dispatchers.Main افتراضياً، ويجب تنفيذ جميع عمليات IO داخله عبر withContext.
الخلاصة
viewModelScope هو خاصية امتداد لواجهة ViewModel، تمت إضافتها في مكتبة lifecycle-viewmodel-ktx (بدءاً من الإصدار 2.1.0). يوفر CoroutineScope جاهزاً للاستخدام مرتبطاً بدورة حياة 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) }
}
يتم إنشاء النطاق بشكل كسول (lazy) عند أول وصول ويتم تخزينه مؤقتاً عبر setTag. يستخدم SupervisorJob، مما يعني أن استثناءً في كوروتين فرعي لا يلغي الآخرين. المُوزع الافتراضي هو Dispatchers.Main.immediate، الذي ينفذ الكود على الخيط الرئيسي دون توزيع إضافي إذا كان الاستدعاء بالفعل على Main.
عندما تغادر ViewModel دورة الحياة (يتم إنهاء Activity أو إزالة Fragment)، يستدعي النظام clear()، الذي يُشغّل onCleared(). في رد الاتصال هذا، يلغي viewModelScope Job الخاص به، مما ينهي بشكل متكرر جميع الكوروتينات النشطة. يتم تنفيذ الآلية من خلال واجهة Closeable، حيث يتم تسجيل Job النطاق كمورد للإغلاق التلقائي.
آلية ربط viewModelScope بدورة حياة ViewModel تستند إلى الوسم (tagging) ورد الاتصال onCleared. دعنا نستعرضها خطوة بخطوة.
عندما تنفذ ViewModel الأمر viewModelScope.launch { ... }، يتحقق getter مما إذا كان هناك نطاق مخزن بالفعل تحت وسم JOB_KEY. إذا لم يكن النطاق موجوداً، يتم إنشاء مثيل جديد CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate). يتم تخزين النطاق داخل ViewModel عبر خريطة وسوم داخلية.
جميع الكوروتينات التي تم إطلاقها عبر viewModelScope.launch أو viewModelScope.async تصبح تابعة لـ SupervisorJob الخاص بالنطاق. تعمل على الخيط الرئيسي (ما لم يتم تحديد مُوزع آخر عبر withContext). طالما أن ViewModel حية، يمكن أن تكون الكوروتينات نشطة أو معلقة أو مكتملة.
عندما يدمر النظام ViewModel، يتم استدعاء ViewModel.clear(). داخل clear()، يحدث ما يلي:
عند تدوير الشاشة، يتم إعادة إنشاء Activity، لكن ViewModel تبقى (بفضل ViewModelStoreOwner). هذا يعني أن viewModelScope يظل نشطاً وتستمر الكوروتينات في التنفيذ دون انقطاع. بعد إعادة إنشاء Activity، يتم إعادة استخدام نفس ViewModel (ونفس النطاق) — لا تبدأ عملية تحميل البيانات من الصفر.
MVVM (Model-View-ViewModel) هي البنية الموصى بها من Google لتطبيقات Android. viewModelScope يحتل مكاناً مركزياً فيها كمنفذ للعمليات غير المتزامنة.
| الطبقة | المكون | دور viewModelScope |
|---|---|---|
| UI | Activity / Fragment | يراقب StateFlow/LiveData من ViewModel |
| ViewModel | ViewModel | يطلق الكوروتينات عبر viewModelScope، يدير حالة واجهة المستخدم |
| Repository | Repository | يقدم دوال suspend تُستدعى من كوروتينات viewModelScope |
| Data | DAO / Api | ينفذ الطلبات الفعلية (Room, Retrofit) |
ViewModel تطلق الكوروتينات عبر viewModelScope، والتي تستدعي داخلها دوال suspend الخاصة بـ Repository. يتم تحويل النتيجة إلى StateFlow، الذي تراقبه طبقة واجهة المستخدم. يضمن هذا التصميم فصلاً واضحاً للمسؤوليات وإمكانية اختبار مستقلة لكل طبقة.
إذا كانت الكوروتينات تُطلق من Fragment، فسيتم إلغاؤها عند تدوير الشاشة مع تدمير Fragment. ViewModel تبقى بعد التدوير، لذا تستمر الكوروتينات المطلقة في نطاقها في التنفيذ. هذه هي الميزة الرئيسية لـ 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، يبدأ تحميل الملف الشخصي فوراً. يتم تنفيذ الكوروتين على الخيط الرئيسي (افتراضياً). يستخدم المستودع withContext(Dispatchers.IO) للطلب الشبكي داخل دالة suspend الخاصة به، لذا لا تهتم 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")
}
}
}
يتم وصف حالة واجهة المستخدم عبر sealed class UiState. تقوم ViewModel بتحديث الحالة عند كل تغيير. 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 مللي ثانية من التوقف. هذا يقلل الحمل على الخادم ويمنع النتائج القديمة.
كلا النطاقين مُقدمان من مكتبة AndroidX Lifecycle، لكنهما مرتبطان بدورتي حياة مختلفتين. يعتمد الاختيار على نوع المهمة.
| الخاصية | viewModelScope | lifecycleScope |
|---|---|---|
| المالك | ViewModel | LifecycleOwner (Activity/Fragment) |
| يُلغى عند التدوير | لا (ViewModel تبقى) | نعم (يتم إعادة إنشاء Activity) |
| الموزع الافتراضي | Dispatchers.Main.immediate | Dispatchers.Main.immediate |
| متوفر في | ViewModel | Activity, Fragment, Service |
| حالة الاستخدام النموذجية | تحميل البيانات، منطق الأعمال | تفاعلات واجهة المستخدم، الرسوم المتحركة |
توصي Google باستخدام viewModelScope لجميع مهام تحميل البيانات ومعالجتها. يجب استخدام lifecycleScope للعمليات المرتبطة بلحظة محددة من دورة حياة واجهة المستخدم — على سبيل المثال، بدء رسم متحرك عند أول ظهور للشاشة أو الاشتراك في تحديثات الموقع التي يجب أن تتوقف عند مغادرة الشاشة.
حتى في API Android الموثق جيداً، يرتكب المطورون أخطاء نموذجية. دعنا نستعرض أربعاً من أكثر المشكلات شيوعاً.
أخطر خطأ هو محاولة تحديث StateFlow أو LiveData بعد مسح ViewModel. على الرغم من إلغاء viewModelScope عند onCleared()، قد ينفذ الكوروتين كوداً قبل أن يصبح الإلغاء ساري المفعول. استخدم isActive للتحقق أو اعتمد على إتمام كتلة catch.
يستخدم viewModelScope SupervisorJob داخلياً، مما يعزل الأخطاء بين الكوروتينات. لكن إذا أطلقت كوروتيناً بـ Job() خاص داخل viewModelScope.launch، يصبح هذا الكوروتين تابعاً لـ SupervisorJob لكنه لن يكون محمياً من الإلغاء الناتج عن أخطاء في كوروتينات أخرى.
على الرغم من أن viewModelScope ليس له حد صارم، فإن آلاف الكوروتينات النشطة قد تبطئ النظام. للقوائم الطويلة من البيانات، استخدم Flow مع collectLatest بدلاً من إنشاء كوروتينات منفصلة لكل عنصر.
إذا تم استيراد GlobalScope عن طريق الخطأ بدلاً من viewModelScope، فلن يتم إلغاء الكوروتين عند مسح ViewModel. يؤدي هذا إلى تسرب الذاكرة واحتمالية تعطل التطبيق. تأكد دائماً من إطلاق الكوروتينات عبر viewModelScope، خاصة في الفئات الفرعية لـ Fragment.
الأسئلة الشائعة
لا يمكنك تغيير موزع viewModelScope مباشرة — فهو مُحدد بشكل ثابت كـ Dispatchers.Main.immediate. لكن داخل الكوروتين يمكنك التبديل إلى موزع آخر عبر withContext. لتغيير الموزع في الاختبارات، استخدم TestDispatcher عبر Rule.
لا تنقل النطاق إلى Repository — هذا يخالف مبادئ البنية. يجب أن يوفر Repository دوال suspend، وViewModel نفسها تدير الكوروتينات عبر viewModelScope. إذا كان Repository يتطلب نطاقاً، فأعد النظر في البنية لصالح Clean Architecture.
SupervisorJob يضمن أن استثناءً في كوروتين واحد (مثل خطأ تحميل في أحد الطلبات المستقلة المتعددة) لا يلغي الكوروتينات الأخرى. هذا يتوافق مع سيناريو ViewModel، حيث تختلف الشاشات في تحميل البيانات المستقلة.
نعم، viewModelScope متوفر في أي ViewModel بغض النظر عن نوع واجهة المستخدم (View System أو Jetpack Compose). في Compose، تُطلق الكوروتينات أيضاً عبر viewModelScope، بينما تُستخدم LaunchedEffect و rememberCoroutineScope لتأثيرات واجهة المستخدم.
استدعاء viewModelScope.cancel() يلغي النطاق فوراً — جميع الكوروتينات النشطة تنتهي بـ CancellationException. إذا تم استدعاء viewModelScope.launch بعد ذلك، يتم إنشاء نطاق جديد تلقائياً عند الوصول التالي إلى getter.
الملخص
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.
اقرأ أيضًا