الكوروتينات في كوتلين تمكنك من كتابة شفرة غير متزامنة واضحة ومبسطة تحافظ على استجابة تطبيقك أثناء إدارة المهام طويلة المدى مثل اتصالات الشبكة أو عمليات القرص.
يوفّر هذا الموضوع نظرة تفصيلية على الكوروتينات على Android. إذا كنت غير معتاد على استخدام الكوروتينات، احرص على قراءة الكوروتينات التي تحتوي على الكوروتينات على Android قبل قراءة هذا الموضوع.
إدارة المهام الطويلة المدى
تعتمد الكوروتينات على الدوال العادية من خلال إضافة عمليتين للتعامل مع المهام طويلة المدى. بالإضافة إلى invoke
(أو call
) وreturn
،
تضيف الكوروتينات 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 - تم تحسين أداة الإرسال هذه لأداء وحدات الإدخال والإخراج على القرص أو الشبكة خارج سلسلة التعليمات الرئيسية. وتشمل الأمثلة استخدام مكوِّن الغرفة، والقراءة من الملفات أو الكتابة إليها، وتشغيل أي عمليات على الشبكة.
- Dispatchers.Default - تم تحسين أداة الإرسال هذه لأداء عمل يستخدم وحدة المعالجة المركزية (CPU) خارج سلسلة التعليمات الرئيسية. تتضمن أمثلة حالات الاستخدام فرز القائمة وتحليل 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()
تُطلق برامج coroutine جديدة باستخدام async
، تستخدم الدالة awaitAll()
لانتظار انتهاء عمليات coroutine الجديدة التي تم إطلاقها قبل العودة. ومع ذلك، يُرجى ملاحظة أنّه حتى لو لم نتواصل مع awaitAll()
، لا تستأنف أداة إنشاء coroutineScope
الكوروتين الذي استدعى fetchTwoDocs
إلا بعد اكتمال جميع الكوروتينات الجديدة.
بالإضافة إلى ذلك، يرصد 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 (...) {
// 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)
}
}
}
مراجع إضافية لعناصر الكوروتين
للحصول على مزيد من موارد الكوروتين، اطلع على الروابط التالية: