CoroutineScope هي واجهة Kotlin تحدد دورة حياة الكوروتين وتوفر سياقاً لتشغيل كوروتينات جديدة. وفقاً لوثائق Kotlin، 2025، يحتوي كل مثيل CoroutineScope على CoroutineContext ويدير جميع الكوروتينات التي تعمل بداخله. عند إلغاء الـ scope، يتم إلغاء جميع الكوروتينات الفرعية تلقائياً، مما يمنع تسرب الذاكرة.
الخلاصة
CoroutineScope هي واجهة أساسية من مكتبة kotlinx.coroutines تعمل كحاوية للكوروتينات. تحدد حدود دورة حياة الكوروتينات: عندما يكتمل الـ scope، يتم إلغاء جميع الكوروتينات داخله تلقائياً.
public interface CoroutineScope {
public val coroutineContext: CoroutineContext
}
تحتوي الواجهة على حقل واحد فقط — coroutineContext. من خلاله، يوفر الـ scope مُوزعاً (Dispatcher)، ومهماً (Job)، ومعالج استثناءات، وعناصر سياق أخرى لجميع الكوروتينات التي تعمل داخله.
جميع دوال تشغيل الكوروتينات — launch، async، runBlocking — هي دوال امتداد على CoroutineScope. وهذا يعني أنه لا يمكن استدعاؤها إلا عند وجود كائن scope. يضمن هذا التصميم أن لكل كوروتين أباً ودورة حياة محددة بوضوح.
في Android، لكل مكون معماري scope خاص به: viewModelScope لـ ViewModel، lifecycleScope لـ Activity/Fragment. في تطبيقات الخادم، يمكن ربط scope بطلب HTTP أو بمجموعة اتصالات قاعدة البيانات.
فهم آلية عمل CoroutineScope الداخلية يتطلب الإلمام بمفهوم Job ومبدأ التزامن الهيكلي.
يعيد كل كوروتين عند تشغيله كائن Job (أو Deferred لـ async). يمثل Job مهمة ذات دورة حياة محدودة: New، Active، Completing، Completed، Cancelling، Cancelled. تشكل كائنات Job بنية شجرية:
التزامن الهيكلي هو مبدأ معماري رئيسي في Kotlin Coroutines، حيث ترتبط دورة حياة الكوروتين بدورة حياة الـ scope الخاص به. يتناقض هذا مع نموذج “أطلق وانسى”، حيث يستمر الكوروتين في الحياة بعد اكتمال الـ scope. مزايا التزامن الهيكلي:
عند استدعاء scope.cancel()، ينتقل Job الخاص بالـ scope إلى الحالة Cancelled، مما يلغي بشكل متكرر جميع Jobs الأبناء. بعد الإلغاء، لا يمكن إعادة استخدام الـ scope إلا إذا تم إنشاء مثيل CoroutineScope جديد.
يمكنك إنشاء CoroutineScope عبر دالة مصنع أو عبر تنفيذ الواجهة في صفك. دعنا نستعرض كلا النهجين.
val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
scope.launch {
println("Running on ${Thread.currentThread().name}")
}
تأخذ دالة المصنع CoroutineContext وتنشئ scope بالسياق المحدد. يستخدم المثال Dispatchers.Default للمهام كثيفة المعالجة وSupervisorJob الذي يعزل الاستثناءات بين الكوروتينات الفرعية.
class MyRepository {
private val scope = CoroutineScope(Dispatchers.IO + Job())
suspend fun fetchData(): Data = scope.async {
api.getData()
}.await()
fun cleanup() {
scope.cancel()
}
}
نخزن الـ scope كحقل صف ونستدعي يدوياً cleanup لإلغائه. هذا النهج مناسب للمكونات ذات دورة حياة مُدارة — على سبيل المثال، المستودعات أو المدراء.
يسمح Kotlin بتفويض تنفيذ CoroutineScope عبر الكلمة المفتاحية by:
class DataLoader : CoroutineScope by CoroutineScope(Dispatchers.IO) {
fun load() {
launch {
// coroutine runs in DataLoader scope
}
}
}
هذا النهج مناسب عندما يكون الصف نفسه scope ويريد توفير طرق تشغيل الكوروتينات. ولكن كن حذراً: يرث الصف جميع طرق CoroutineScope، بما في ذلك cancel، مما قد يخرق التغليف.
GlobalScope هو CoroutineScope فردي (singleton) للتطبيق بالكامل. استخدامه في كود الإنتاج غير موصى به رسمياً.
تسمح JetBrains باستخدام GlobalScope فقط في السيناريوهات النادرة: العمليات الخلفية على مستوى التطبيق التي يجب أن تستمر حتى بعد إغلاق جميع الـ Activities (مثل مزامنة البيانات، التحليلات). ولكن حتى في هذه الحالات، يُفضل إنشاء scope خاص بك باستخدام CoroutineScope(SupervisorJob()).
استخدم دائماً CoroutineScope مخصصاً مع إدارة دورة حياة واضحة. في Android، هذه هي viewModelScope وlifecycleScope. في تطبيقات الخادم، أنشئ scope لكل طلب أو مجموعة اتصالات.
كلتا الدالتين هما دالتان معلقتان (suspend) تنشئان scope مؤقتاً للمهام المتوازية، لكن سلوكهما مع الاستثناءات يختلف جوهرياً.
| الخاصية | coroutineScope | supervisorScope |
|---|---|---|
| السلوك عند الخطأ | استثناء في كوروتين فرعي يلغي جميع الآخرين | استثناء في كوروتين فرعي لا يلغي الآخرين |
| نشر الخطأ | نعم، يتم نشر أول استثناء إلى الخارج | نعم، يتم نشر أول استثناء إلى الخارج |
| Job الافتراضي | Job() — الأبناء مرتبطون بالأب | SupervisorJob() — الأبناء لا يعتمدون على بعضهم |
| حالة الاستخدام النموذجية | عملية ذرية متعددة الخطوات | مهام متوازية مستقلة (تحميلات UI) |
استخدم coroutineScope عندما تشكل عدة عمليات متوازية عملية ذرية واحدة. على سبيل المثال، تحميل بيانات من ثلاثة خوادم: إذا فشل طلب واحد، لا معنى للبقية.
suspend fun loadProductPage(): ProductPage = coroutineScope {
val product = async { api.getProduct() }
val reviews = async { api.getReviews() }
ProductPage(product.await(), reviews.await())
}
إذا ألقى getProduct أو getReviews استثناءً — يتم إلغاء كلا الكوروتينين، وينتشر الاستثناء إلى الكود المستدعي.
استخدم supervisorScope عندما لا تعتمد العمليات المتوازية على بعضها. على سبيل المثال، تحميل بيانات الملف الشخصي في عدة أقسام مستقلة: إذا فشل قسم التوصيات، يجب أن يظهر رأس الملف الشخصي وقائمة الأصدقاء.
دعنا نستعرض الأخطاء الأكثر شيوعاً للمطورين عند استخدام CoroutineScope في Kotlin.
السيناريو الأكثر شيوعاً لتسرب الكوروتينات هو إنشاء scope دون استدعاء cancel عند انتهاء المكون. إذا لم يتم إلغاء الـ scope، تستمر الكوروتينات في العمل، مبقية المراجع على الكائنات. في Android، استخدم viewModelScope أو lifecycleScope، والتي تُلغى تلقائياً.
يتجاهل GlobalScope دورة حياة مكونات Android. كوروتين شُغِّل في GlobalScope بعد إغلاق Activity سيستمر في التنفيذ وسيحاول تحديث UI — مما يؤدي إلى تعطل. استخدم دائماً lifecycleScope لمكونات UI.
بعد استدعاء cancel()، لا يمكن إعادة استخدام الـ scope — جميع الكوروتينات داخله قد اكتملت بالفعل. أنشئ مثيل CoroutineScope جديداً عبر دالة المصنع. Job() لا يدعم إعادة التنشيط.
عند التفويض بـ by، يحصل الصف على طريقة cancel() عامة يمكن استدعاؤها من أي مكان، مما يخرق التغليف. خزّن الـ scope كحقل خاص بدلاً من تفويض الواجهة.
الأسئلة الشائعة
CoroutineScope هي واجهة تمتلك CoroutineContext وتكون مسؤولة عن دورة حياة الكوروتينات. CoroutineContext هو مجموعة من العناصر (الموزع، job، معالج الأخطاء) تحدد “كيف” يتم تنفيذ الكوروتين. أحد الفروق: الـ scope ينشئ الكوروتينات، بينما يتحكم السياق في سلوكها.
نعم، هذا نمط قياسي: CoroutineScope(Dispatchers.IO + SupervisorJob()). يمنع SupervisorJob الإلغاء المتسلسل للكوروتينات الفرعية عند حدوث استثناء في أحدها. هذا مفيد للمهام المتوازية المستقلة حيث لا يجب أن يوقف خطأ في إحداها الأخرى.
لا يوجد حد لعدد الكوروتينات في scope — فهي محدودة فقط بالذاكرة المتاحة وإعدادات الموزع. الحد العملي عادة هو آلاف الكوروتينات النشطة في scope واحد. ومع ذلك، قد يشير العدد الكبير من الكوروتينات إلى مشاكل معمارية.
الطريقة الصحيحة هي تمرير الـ scope إلى الصف عبر المُنشئ أو استخدام runBlockingTest / runTest من kotlinx-coroutines-test. في الاختبارات، يمكنك استبدال الـ scope بـ TestCoroutineDispatcher والتحكم في تنفيذ الكوروتينات يدوياً.
لا، الـ scope هو حاوية خارجية للكوروتين. الكوروتين نفسه ليس scope. ومع ذلك، داخل الكوروتين يمكنك إنشاء scope جديد عبر coroutineScope أو supervisorScope لتشغيل كوروتينات فرعية بالتوازي.
الملخص
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.
اقرأ أيضًا