تتيح لك روتينات 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 المتزامنة إلى تنفيذ العمل على أداة الإرسال Default أو IO. في Kotlin، يجب تشغيل جميع الروتينات المشتركة في أداة إرسال، حتى عند تشغيلها في سلسلة التعليمات الرئيسية. يمكن أن تعلّق الروتينات الفرعية نفسها، ويكون المرسل مسؤولاً عن استئنافها.
لتحديد المكان الذي يجب أن يتم فيه تنفيذ الكوروتينات، يوفّر Kotlin ثلاثة أدوات إرسال يمكنك استخدامها:
- Dispatchers.Main: استخدِم أداة الإرسال هذه لتشغيل روتين فرعي على سلسلة التعليمات الرئيسية في Android. يجب استخدامها فقط للتفاعل مع واجهة المستخدم وتنفيذ المهام السريعة. وتشمل الأمثلة استدعاء دوال
suspendوتنفيذ عمليات إطار عمل واجهة مستخدم Android وتعديل عناصرLiveData. - Dispatchers.IO: تم تحسين أداء أداة الإرسال هذه لتنفيذ عمليات الإدخال والإخراج على القرص أو الشبكة خارج سلسلة التعليمات الرئيسية. وتشمل الأمثلة استخدام مكوّن الغرفة، القراءة من الملفات أو الكتابة فيها، وتنفيذ أي عمليات على الشبكة.
- 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 باستخدام أنماط "كوروتين".
ومع ذلك، إذا كنت بحاجة إلى إنشاء 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) } } }
مراجع إضافية حول الروتينات المشتركة
لمزيد من المراجع حول الروتينات المشتركة، يُرجى الاطّلاع على الروابط التالية: