viewModelScope: ما هو، الربط بـ ViewModel وكيف يعمل في Android

المؤلف: IT Sectr نُشر: 2026-06-23 وقت القراءة: 9 دق

viewModelScope هو CoroutineScope مدمج من مكتبة androidx.lifecycle يرتبط بدورة حياة ViewModel ويتم إلغاؤه تلقائياً عند مسحها. وفقاً لـ Google Android Developers, 2025، فإن viewModelScope هو الآلية القياسية لإطلاق الكوروتينات في بنية MVVM، مما يضمن عمليات غير متزامنة آمنة دون خطر تسرب الذاكرة. يستخدم ViewModelScope Dispatchers.Main افتراضياً، ويجب تنفيذ جميع عمليات IO داخله عبر withContext.

الخلاصة

  • viewModelScope — CoroutineScope من lifecycle-viewmodel-ktx، يُلغى عند استدعاء ViewModel.onCleared()
  • Dispatchers.Main — المُوزع الافتراضي، لذا فإن تحديثات واجهة المستخدم داخل الكوروتينات آمنة
  • onCleared — رد اتصال يُفعّل الإلغاء التلقائي لجميع الكوروتينات النشطة في viewModelScope
  • clear() vs onCleared() — clear() يُستدعى من الإطار قبل onCleared، مما يضمن إلغاء النطاق
  • launch — الطريقة الأساسية لبدء الكوروتينات في viewModelScope لعمليات الإطلاق والنسيان

ما هو viewModelScope في Android؟

viewModelScope هو خاصية امتداد لواجهة ViewModel، تمت إضافتها في مكتبة lifecycle-viewmodel-ktx (بدءاً من الإصدار 2.1.0). يوفر CoroutineScope جاهزاً للاستخدام مرتبطاً بدورة حياة 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) }
    }

يتم إنشاء النطاق بشكل كسول (lazy) عند أول وصول ويتم تخزينه مؤقتاً عبر setTag. يستخدم SupervisorJob، مما يعني أن استثناءً في كوروتين فرعي لا يلغي الآخرين. المُوزع الافتراضي هو Dispatchers.Main.immediate، الذي ينفذ الكود على الخيط الرئيسي دون توزيع إضافي إذا كان الاستدعاء بالفعل على Main.

كيف يتلقى viewModelScope إشعار المسح

عندما تغادر ViewModel دورة الحياة (يتم إنهاء Activity أو إزالة Fragment)، يستدعي النظام clear()، الذي يُشغّل onCleared(). في رد الاتصال هذا، يلغي viewModelScope Job الخاص به، مما ينهي بشكل متكرر جميع الكوروتينات النشطة. يتم تنفيذ الآلية من خلال واجهة Closeable، حيث يتم تسجيل Job النطاق كمورد للإغلاق التلقائي.

كيف يعمل viewModelScope: الربط بدورة حياة ViewModel

آلية ربط viewModelScope بدورة حياة ViewModel تستند إلى الوسم (tagging) ورد الاتصال onCleared. دعنا نستعرضها خطوة بخطوة.

الخطوة 1: إنشاء النطاق عند أول وصول

عندما تنفذ ViewModel الأمر viewModelScope.launch { ... }، يتحقق getter مما إذا كان هناك نطاق مخزن بالفعل تحت وسم JOB_KEY. إذا لم يكن النطاق موجوداً، يتم إنشاء مثيل جديد CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate). يتم تخزين النطاق داخل ViewModel عبر خريطة وسوم داخلية.

الخطوة 2: دورة حياة الكوروتينات

جميع الكوروتينات التي تم إطلاقها عبر viewModelScope.launch أو viewModelScope.async تصبح تابعة لـ SupervisorJob الخاص بالنطاق. تعمل على الخيط الرئيسي (ما لم يتم تحديد مُوزع آخر عبر withContext). طالما أن ViewModel حية، يمكن أن تكون الكوروتينات نشطة أو معلقة أو مكتملة.

الخطوة 3: الإلغاء عند onCleared

عندما يدمر النظام ViewModel، يتم استدعاء ViewModel.clear(). داخل clear()، يحدث ما يلي:

  • استدعاء onCleared() لمنطق التنظيف المخصص
  • إغلاق جميع موارد Closeable المسجلة عبر addCloseable
  • انتقال Job الخاص بـ viewModelScope إلى حالة Cancelled
  • إلغاء جميع الكوروتينات التابعة بشكل متكرر
  • تحرير المراجع إلى النطاق لمجمع القمامة

مقاومة التدوير

عند تدوير الشاشة، يتم إعادة إنشاء Activity، لكن ViewModel تبقى (بفضل ViewModelStoreOwner). هذا يعني أن viewModelScope يظل نشطاً وتستمر الكوروتينات في التنفيذ دون انقطاع. بعد إعادة إنشاء Activity، يتم إعادة استخدام نفس ViewModel (ونفس النطاق) — لا تبدأ عملية تحميل البيانات من الصفر.

viewModelScope في بنية MVVM

MVVM (Model-View-ViewModel) هي البنية الموصى بها من Google لتطبيقات Android. viewModelScope يحتل مكاناً مركزياً فيها كمنفذ للعمليات غير المتزامنة.

دور viewModelScope في طبقات البنية

الطبقةالمكوندور viewModelScope
UIActivity / Fragmentيراقب StateFlow/LiveData من ViewModel
ViewModelViewModelيطلق الكوروتينات عبر viewModelScope، يدير حالة واجهة المستخدم
RepositoryRepositoryيقدم دوال suspend تُستدعى من كوروتينات viewModelScope
DataDAO / Apiينفذ الطلبات الفعلية (Room, Retrofit)

ViewModel تطلق الكوروتينات عبر viewModelScope، والتي تستدعي داخلها دوال suspend الخاصة بـ Repository. يتم تحويل النتيجة إلى StateFlow، الذي تراقبه طبقة واجهة المستخدم. يضمن هذا التصميم فصلاً واضحاً للمسؤوليات وإمكانية اختبار مستقلة لكل طبقة.

لماذا viewModelScope في ViewModel وليس في Fragment

إذا كانت الكوروتينات تُطلق من Fragment، فسيتم إلغاؤها عند تدوير الشاشة مع تدمير Fragment. ViewModel تبقى بعد التدوير، لذا تستمر الكوروتينات المطلقة في نطاقها في التنفيذ. هذه هي الميزة الرئيسية لـ viewModelScope على lifecycleScope عند تحميل البيانات.

أمثلة استخدام viewModelScope

دعنا نستعرض ثلاثة سيناريوهات عملية لاستخدام viewModelScope في تطبيق Android بلغة Kotlin.

مثال 1: تحميل البيانات عند إنشاء 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
        }
    }
}

في كتلة init، يبدأ تحميل الملف الشخصي فوراً. يتم تنفيذ الكوروتين على الخيط الرئيسي (افتراضياً). يستخدم المستودع withContext(Dispatchers.IO) للطلب الشبكي داخل دالة suspend الخاصة به، لذا لا تهتم ViewModel بتبديل الخيوط.

مثال 2: معالجة الأخطاء باستخدام 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")
        }
    }
}

يتم وصف حالة واجهة المستخدم عبر sealed class UiState. تقوم ViewModel بتحديث الحالة عند كل تغيير. Fragment مشترك على StateFlow ويتفاعل فقط مع الحالة الحالية، متجاهلاً الاستدعاءات القديمة من التدويرات السابقة.

مثال 3: إلغاء الكوروتين السابق عند طلب جديد

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
    }
}

عند كل استعلام بحث جديد، يتم إلغاء الكوروتين السابق. delay(300) ينفذ debounce — يتم البحث فقط بعد 300 مللي ثانية من التوقف. هذا يقلل الحمل على الخادم ويمنع النتائج القديمة.

viewModelScope ضد lifecycleScope: متى تختار أيهما

كلا النطاقين مُقدمان من مكتبة AndroidX Lifecycle، لكنهما مرتبطان بدورتي حياة مختلفتين. يعتمد الاختيار على نوع المهمة.

مقارنة النطاقات

الخاصيةviewModelScopelifecycleScope
المالكViewModelLifecycleOwner (Activity/Fragment)
يُلغى عند التدويرلا (ViewModel تبقى)نعم (يتم إعادة إنشاء Activity)
الموزع الافتراضيDispatchers.Main.immediateDispatchers.Main.immediate
متوفر فيViewModelActivity, Fragment, Service
حالة الاستخدام النموذجيةتحميل البيانات، منطق الأعمالتفاعلات واجهة المستخدم، الرسوم المتحركة

توصيات Google

توصي Google باستخدام viewModelScope لجميع مهام تحميل البيانات ومعالجتها. يجب استخدام lifecycleScope للعمليات المرتبطة بلحظة محددة من دورة حياة واجهة المستخدم — على سبيل المثال، بدء رسم متحرك عند أول ظهور للشاشة أو الاشتراك في تحديثات الموقع التي يجب أن تتوقف عند مغادرة الشاشة.

الأخطاء الشائعة عند العمل مع viewModelScope

حتى في API Android الموثق جيداً، يرتكب المطورون أخطاء نموذجية. دعنا نستعرض أربعاً من أكثر المشكلات شيوعاً.

الخطأ 1: تحديث واجهة المستخدم بعد إلغاء النطاق

أخطر خطأ هو محاولة تحديث StateFlow أو LiveData بعد مسح ViewModel. على الرغم من إلغاء viewModelScope عند onCleared()، قد ينفذ الكوروتين كوداً قبل أن يصبح الإلغاء ساري المفعول. استخدم isActive للتحقق أو اعتمد على إتمام كتلة catch.

الخطأ 2: إطلاق الكوروتينات دون مراعاة SupervisorJob

يستخدم viewModelScope SupervisorJob داخلياً، مما يعزل الأخطاء بين الكوروتينات. لكن إذا أطلقت كوروتيناً بـ Job() خاص داخل viewModelScope.launch، يصبح هذا الكوروتين تابعاً لـ SupervisorJob لكنه لن يكون محمياً من الإلغاء الناتج عن أخطاء في كوروتينات أخرى.

الخطأ 3: عدد كبير جداً من الكوروتينات في نطاق واحد

على الرغم من أن viewModelScope ليس له حد صارم، فإن آلاف الكوروتينات النشطة قد تبطئ النظام. للقوائم الطويلة من البيانات، استخدم Flow مع collectLatest بدلاً من إنشاء كوروتينات منفصلة لكل عنصر.

الخطأ 4: استخدام GlobalScope بدلاً من viewModelScope

إذا تم استيراد GlobalScope عن طريق الخطأ بدلاً من viewModelScope، فلن يتم إلغاء الكوروتين عند مسح ViewModel. يؤدي هذا إلى تسرب الذاكرة واحتمالية تعطل التطبيق. تأكد دائماً من إطلاق الكوروتينات عبر viewModelScope، خاصة في الفئات الفرعية لـ Fragment.

الأسئلة الشائعة

هل يمكنني تغيير الموزع الافتراضي لـ viewModelScope؟

لا يمكنك تغيير موزع viewModelScope مباشرة — فهو مُحدد بشكل ثابت كـ Dispatchers.Main.immediate. لكن داخل الكوروتين يمكنك التبديل إلى موزع آخر عبر withContext. لتغيير الموزع في الاختبارات، استخدم TestDispatcher عبر Rule.

كيف أنقل viewModelScope إلى Repository؟

لا تنقل النطاق إلى Repository — هذا يخالف مبادئ البنية. يجب أن يوفر Repository دوال suspend، وViewModel نفسها تدير الكوروتينات عبر viewModelScope. إذا كان Repository يتطلب نطاقاً، فأعد النظر في البنية لصالح Clean Architecture.

لماذا يستخدم viewModelScope SupervisorJob؟

SupervisorJob يضمن أن استثناءً في كوروتين واحد (مثل خطأ تحميل في أحد الطلبات المستقلة المتعددة) لا يلغي الكوروتينات الأخرى. هذا يتوافق مع سيناريو ViewModel، حيث تختلف الشاشات في تحميل البيانات المستقلة.

هل viewModelScope متوفر في Jetpack Compose؟

نعم، viewModelScope متوفر في أي ViewModel بغض النظر عن نوع واجهة المستخدم (View System أو Jetpack Compose). في Compose، تُطلق الكوروتينات أيضاً عبر viewModelScope، بينما تُستخدم LaunchedEffect و rememberCoroutineScope لتأثيرات واجهة المستخدم.

ماذا يحدث للكوروتين عند استدعاء viewModelScope.cancel()؟

استدعاء viewModelScope.cancel() يلغي النطاق فوراً — جميع الكوروتينات النشطة تنتهي بـ CancellationException. إذا تم استدعاء viewModelScope.launch بعد ذلك، يتم إنشاء نطاق جديد تلقائياً عند الوصول التالي إلى getter.

الملخص

  • viewModelScope — CoroutineScope مرتبط بدورة حياة ViewModel، يُلغى تلقائياً عند onCleared()
  • SupervisorJob + Dispatchers.Main — تكوين داخلي يضمن عزل الأخطاء والوصول الآمن إلى واجهة المستخدم
  • تدوير الشاشة — ViewModel تبقى، لذا تستمر الكوروتينات في viewModelScope دون إعادة تشغيل
  • بنية MVVM — viewModelScope هو العنصر المركزي للعمليات غير المتزامنة في طبقة ViewModel
  • lifecycleScope — بديل للعمليات المرتبطة بدورة حياة Activity/Fragment، وليس ViewModel
  • StateFlow — الطريقة المفضلة لنقل البيانات من كوروتينات viewModelScope إلى واجهة المستخدم عبر sealed class
  • GlobalScope خطر — استبدال viewModelScope بـ GlobalScope يؤدي إلى تسرب الذاكرة وتعطل التطبيق

سنقوم بتطوير تطبيق جوال جاهز

تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.

مناقشة المشروع

اقرأ أيضًا