تتيح لك الروتينات المشتركة في Kotlin كتابة رمز غير متزامن بسيط ونظيف يحافظ على استجابة تطبيقك أثناء إدارة المهام الطويلة الأمد، مثل طلبات الشبكة أو عمليات القرص.
يقدّم هذا الموضوع نظرة تفصيلية على الكوروتينات في Android. إذا لم تكن على دراية بالكوروتينات، احرص على قراءة الكوروتينات في Kotlin على Android قبل قراءة هذا الموضوع.
إدارة المهام الطويلة المدى
تعتمد الروتينات المشتركة على الدوال العادية من خلال إضافة عمليتَين للتعامل مع المهام التي تستغرق وقتًا طويلاً. بالإضافة إلى invoke (أو call) وreturn،
تضيف إجراءات coroutines suspend وresume:
- يؤدي
suspendإلى إيقاف تنفيذ الروتين الفرعي الحالي مؤقتًا، مع حفظ جميع المتغيرات المحلية. - تواصل الدالة الفرعية
resumeتنفيذ دالة فرعية معلّقة من الموضع الذي تم تعليقها فيه.
يمكنك استدعاء دوال suspend فقط من دوال suspend أخرى أو باستخدام أداة إنشاء كوروتين مثل launch لبدء كوروتين جديد.
يوضّح المثال التالي عملية تنفيذ بسيطة لبرنامج فرعي متزامن لمهمة افتراضية تستغرق وقتًا طويلاً:
suspend fun fetchDocs() { // Dispatchers.Main val result = get("https://developer.android.com") // Dispatchers.IO for `get` show(result) // Dispatchers.Main } suspend fun get(url: String) = withContext(Dispatchers.IO) { }
في هذا المثال، يستمر تنفيذ get() على سلسلة التعليمات الرئيسية، ولكنّه يعلّق الروتين الفرعي قبل بدء طلب الشبكة. عند اكتمال طلب الشبكة، تستأنف الدالة الفرعية المعلقة get بدلاً من استخدام دالة رد نداء لإعلام سلسلة التعليمات الرئيسية.
تستخدم Kotlin إطار حزمة لإدارة الدالة التي يتم تنفيذها مع أي متغيرات محلية. عند تعليق روتين فرعي، يتم نسخ إطار حزمة البيانات الحالي وحفظه لاستخدامه لاحقًا. عند الاستئناف، يتم نسخ إطار التجميع مرة أخرى من المكان الذي تم حفظه فيه، وتبدأ الدالة في العمل مرة أخرى. على الرغم من أنّ الرمز البرمجي قد يبدو كطلب حظر تسلسلي عادي، تضمن الروتين الفرعي عدم حظر طلب الشبكة لسلسلة التعليمات الرئيسية.
استخدام الروتينات المشتركة لضمان أمان سلسلة التعليمات الرئيسية
تستخدِم إجراءات Kotlin الفرعية عوامل الإرسال لتحديد سلاسل المحادثات المستخدَمة لتنفيذ الإجراءات الفرعية. لتشغيل الرمز البرمجي خارج سلسلة التعليمات الرئيسية، يمكنك توجيه إجراءات Kotlin المتزامنة إلى تنفيذ العمل على أداة الإرسال الافتراضية أو IO. في Kotlin، يجب أن تعمل جميع الروتينات الفرعية في أداة إرسال، حتى عندما تعمل على سلسلة التعليمات الرئيسية. يمكن أن يتم تعليق العمليات الروتينية، ويكون المرسل مسؤولاً عن استئنافها.
لتحديد المكان الذي يجب أن يتم فيه تنفيذ الروتينات الفرعية، توفّر Kotlin ثلاثة أدوات إرسال يمكنك استخدامها:
- Dispatchers.Main: استخدِم أداة الإرسال هذه لتشغيل روتين فرعي في سلسلة التعليمات الرئيسية على Android. يجب استخدامها فقط للتفاعل مع واجهة المستخدم وتنفيذ مهام سريعة. وتشمل الأمثلة استدعاء دوال
suspendوتنفيذ عمليات إطار عمل واجهة مستخدم Android وتعديل عناصرLiveData. - Dispatchers.IO: تم تحسين أداة الإرسال هذه لتنفيذ عمليات الإدخال والإخراج على القرص أو الشبكة خارج سلسلة التعليمات الرئيسية. وتشمل الأمثلة استخدام مكوّن Room، والقراءة من الملفات أو الكتابة فيها، وتنفيذ أي عمليات على الشبكة.
- Dispatchers.Default: تم تحسين أداة الإرسال هذه لتنفيذ المهام التي تتطلّب استخدام وحدة المعالجة المركزية بشكل مكثّف خارج سلسلة التعليمات الرئيسية. تشمل الأمثلة على حالات الاستخدام ترتيب قائمة وتحليل JSON.
بالاستناد إلى المثال السابق، يمكنك استخدام أدوات الإرسال لإعادة تعريف الدالة get. داخل نص get، استدعِ withContext(Dispatchers.IO) لإنشاء عملية حظر يتم تنفيذها في مجموعة سلاسل IO. يتم دائمًا تنفيذ أي رمز تضعه داخل هذا الحظر من خلال أداة الإرسال IO. بما أنّ withContext هي نفسها دالة تعليق، فإنّ الدالة get هي أيضًا دالة تعليق.
suspend fun fetchDocs() { // Dispatchers.Main val result = get("developer.android.com") // Dispatchers.Main show(result) // Dispatchers.Main } suspend fun get(url: String) = // Dispatchers.Main withContext(Dispatchers.IO) { // Dispatchers.IO (main-safety block) /* perform network IO here */ // Dispatchers.IO (main-safety block) } // Dispatchers.Main
باستخدام الروتينات المشتركة، يمكنك إرسال سلاسل محادثات مع إمكانية التحكّم الدقيق. بما أنّ
withContext() تتيح لك التحكّم في مجموعة سلاسل التعليمات لأي سطر من الرموز البرمجية بدون
إضافة دوال رد الاتصال، يمكنك تطبيقها على دوال صغيرة جدًا، مثل القراءة
من قاعدة بيانات أو تنفيذ طلب شبكة. من الممارسات الجيدة استخدام
withContext() للتأكّد من أنّ كل دالة آمنة للاستخدام في سلسلة التعليمات الرئيسية، ما يعني أنّه يمكنك
استدعاء الدالة من سلسلة التعليمات الرئيسية. بهذه الطريقة، لن يحتاج المتصل إلى التفكير في سلسلة التعليمات البرمجية التي يجب استخدامها لتنفيذ الدالة.
في المثال السابق، يتم تنفيذ fetchDocs() في سلسلة التعليمات الرئيسية، ولكن يمكنه استدعاء get بأمان، ما يؤدي إلى تنفيذ طلب شبكة في الخلفية.
بما أنّ الروتينات الفرعية تتوافق مع suspend وresume، تتم استعادة الروتين الفرعي في سلسلة التعليمات الرئيسية باستخدام نتيجة get فور انتهاء حظر withContext.
أداء withContext()
لا يضيف withContext() أي حمل إضافي مقارنةً بالتنفيذ المكافئ المستند إلى معاودة الاتصال. علاوةً على ذلك، من الممكن تحسين استدعاءات withContext() بما يتجاوز التنفيذ المكافئ المستند إلى معاودة الاتصال في بعض الحالات. على سبيل المثال، إذا كانت إحدى الدوال تجري عشر عمليات طلب إلى شبكة، يمكنك إخبار Kotlin بتبديل سلاسل المحادثات مرة واحدة فقط باستخدام withContext() خارجي. بعد ذلك، على الرغم من أنّ مكتبة الشبكة تستخدم withContext() عدة مرات، إلا أنّها تبقى على أداة الإرسال نفسها وتتجنّب تبديل سلاسل المحادثات. بالإضافة إلى ذلك، تعمل Kotlin على تحسين التبديل بين Dispatchers.Default وDispatchers.IO لتجنُّب التبديل بين سلاسل التنفيذ كلما أمكن ذلك.
بدء روتين فرعي
يمكنك بدء إجراءات روتينية مشتركة بإحدى الطريقتَين التاليتَين:
- يبدأ
launchروتينًا فرعيًا جديدًا ولا يعرض النتيجة على المتصل. يمكن بدء أي عمل يُعدّ "مهمة سريعة" باستخدامlaunch. - يبدأ
asyncكوروتين جديدًا ويسمح لك بعرض نتيجة باستخدام وظيفة تعليق تُسمىawait.
عادةً، يجب launch روتينًا فرعيًا جديدًا من دالة عادية،
لأنّ الدالة العادية لا يمكنها استدعاء await. استخدِم async فقط عندما تكون داخل روتين فرعي آخر أو عندما تكون داخل دالة تعليق وتنفيذ تحليل متوازٍ.
التقسيم المتوازي
يجب إيقاف جميع الروتينات المشتركة التي يتم بدء تشغيلها داخل دالة suspend عند انتهاء هذه الدالة، لذا من المحتمل أن تحتاج إلى ضمان انتهاء هذه الروتينات المشتركة قبل انتهاء الدالة. باستخدام التزامن المنظَّم في Kotlin، يمكنك تحديد coroutineScope يبدأ روتينًا فرعيًا واحدًا أو أكثر. بعد ذلك، باستخدام await()
(لإجراء روتيني فرعي واحد) أو awaitAll() (لإجراءات روتينية فرعية متعددة)، يمكنك
ضمان انتهاء هذه الإجراءات الروتينية الفرعية قبل الرجوع من الدالة.
كمثال، لنحدّد coroutineScope يجلب مستندَين بشكل غير متزامن. من خلال استدعاء await() لكل مرجع مؤجّل، نضمن
انتهاء عمليتَي async قبل عرض قيمة:
suspend fun fetchTwoDocs() = coroutineScope { val deferredOne = async { fetchDoc(1) } val deferredTwo = async { fetchDoc(2) } deferredOne.await() deferredTwo.await() }
يمكنك أيضًا استخدام awaitAll() في المجموعات، كما هو موضّح في المثال التالي:
suspend fun fetchTwoDocs() = // called on any Dispatcher (any thread, possibly Main) coroutineScope { val deferreds = listOf( // fetch two docs at the same time async { fetchDoc(1) }, // async returns a result for the first doc async { fetchDoc(2) } // async returns a result for the second doc ) deferreds.awaitAll() // use awaitAll to wait for both network requests }
على الرغم من أنّ fetchTwoDocs() تُطلق كوروتينات جديدة باستخدام async، تستخدم الدالة awaitAll() لانتظار انتهاء الكوروتينات التي تم إطلاقها قبل أن تعرض النتيجة. يُرجى العِلم أنّه حتى لو لم نستدعِ awaitAll()، لن يستأنف منشئ coroutineScope الروتين الفرعي الذي استدعى fetchTwoDocs إلا بعد اكتمال جميع الروتينات الفرعية الجديدة.
بالإضافة إلى ذلك، يرصد coroutineScope أي استثناءات تطرحها الروتينات الفرعية
ويعيد توجيهها إلى المتصل.
لمزيد من المعلومات حول التجزئة المتوازية، راجِع مقالة إنشاء دوال تعليق.
مفاهيم أنماط "كوروتين"
CoroutineScope
يتتبّع CoroutineScope أي كوروتين ينشئه باستخدام launch أو async. يمكن إلغاء العمل الجاري (أي إجراءات الروتين التي يتم تنفيذها) من خلال استدعاء scope.cancel() في أي وقت. في Android، توفّر بعض مكتبات KTX CoroutineScope خاصًا بها لفئات دورة الحياة معيّنة. على سبيل المثال، يتضمّن ViewModel viewModelScope، ويتضمّن Lifecycle lifecycleScope.
على عكس أداة الإرسال، لا تنفّذ CoroutineScope الروتينات الفرعية.
يتم استخدام viewModelScope أيضًا في الأمثلة الواردة في تنفيذ سلاسل التعليمات في الخلفية على Android باستخدام Coroutines.
ومع ذلك، إذا كنت بحاجة إلى إنشاء CoroutineScope خاص بك للتحكّم في دورة حياة الروتينات الفرعية في طبقة معيّنة من تطبيقك، يمكنك إنشاء CoroutineScope على النحو التالي:
class ExampleClass { // Job and Dispatcher are combined into a CoroutineContext which // will be discussed shortly val scope = CoroutineScope(Job() + Dispatchers.Main) fun exampleMethod() { // Starts a new coroutine within the scope scope.launch { // New coroutine that can call suspend functions fetchDocs() } } fun cleanUp() { // Cancel the scope to cancel ongoing coroutines work scope.cancel() } }
لا يمكن لنطاق تم إلغاؤه إنشاء المزيد من الروتينات الفرعية. لذلك، يجب استدعاء scope.cancel() فقط عند إيقاف الفئة التي تتحكّم في دورة حياتها. عند استخدام viewModelScope، يلغي الصف
ViewModel النطاق تلقائيًا في طريقة onCleared() الخاصة بـ ViewModel.
وظيفة
Job هو معرّف لكوروتين. تعرض كل روتين فرعي تنشئه باستخدام launch
أو async مثيلاً من Job يحدّد الروتين الفرعي بشكل فريد ويدير دورة حياته. يمكنك أيضًا تمرير Job إلى CoroutineScope لإدارة دورة حياته بشكل أكبر، كما هو موضّح في المثال التالي:
class ExampleClass { // ... fun exampleMethod() { // Handle to the coroutine, you can control its lifecycle val job = scope.launch { // New coroutine } if (condition) { // Cancel the coroutine started above, this doesn't affect the scope // this coroutine was launched in job.cancel() } } }
CoroutineContext
تحدّد CoroutineContext سلوك الروتين الفرعي باستخدام مجموعة العناصر التالية:
-
Job: تتحكّم في مراحل نشاط الكوروتين. -
CoroutineDispatcher: ترسل هذه السمة العمل إلى سلسلة التعليمات البرمجية المناسبة. -
CoroutineName: اسم الكوروتين، وهو مفيد لتصحيح الأخطاء. -
CoroutineExceptionHandler: تعالج الاستثناءات غير المعالَجة.
بالنسبة إلى الروتينات الفرعية الجديدة التي يتم إنشاؤها ضمن نطاق، يتم تعيين مثيل Job جديد إلى الروتين الفرعي الجديد، ويتم اكتساب عناصر CoroutineContext الأخرى من النطاق الحاوي. يمكنك تجاهل العناصر الموروثة من خلال تمرير CoroutineContext جديد إلى الدالة launch أو async. يُرجى العِلم أنّ تمرير Job إلى launch أو async ليس له أي تأثير،
لأنّه يتم دائمًا تعيين مثيل جديد من Job إلى روتين فرعي جديد.
class ExampleClass { val scope = CoroutineScope(Job() + Dispatchers.Main) fun exampleMethod() { // Starts a new coroutine on Dispatchers.Main as it's the scope's default val job1 = scope.launch { // New coroutine with CoroutineName = "coroutine" (default) } // Starts a new coroutine on Dispatchers.Default val job2 = scope.launch(Dispatchers.Default + CoroutineName("BackgroundCoroutine")) { // New coroutine with CoroutineName = "BackgroundCoroutine" (overridden) } } }
مراجع إضافية حول الروتينات المشتركة
لمزيد من المراجع حول الروتينات المشتركة، يُرجى الاطّلاع على الروابط التالية: