
قررت أن أكتب عن بعض الأشياء التي ، في رأيي ، يجب تجنبها عند استخدام Kotlin coroutine.
لف المكالمات غير المتزامنة في coroutineScope أو استخدم SupervisorJob للتعامل مع الاستثناءات
إذا حدث استثناء في كتلة async
، فلا تعتمد على try/catch
.
val job: Job = Job() val scope = CoroutineScope(Dispatchers.Default + job)
في المثال أعلاه ، تبدأ وظيفة doWork
coroutine (1) جديدة ، والتي قد تتسبب في استثناء غير معالَج. إذا حاولت التفاف doWork
باستخدام doWork
try/catch
(2) ، فسيظل التطبيق يتعطل.
وذلك لأن فشل أي عنصر تابع في الوظيفة يؤدي إلى الفشل الفوري لوالدها.
طريقة واحدة لتجنب الخطأ هي استخدام SupervisorJob
(1).
لن يؤدي فشل أو إلغاء تنفيذ المكون الفرعي إلى فشل الوالد ولن يؤثر على المكونات الأخرى.
val job = SupervisorJob()
ملاحظة : لن يعمل هذا إلا إذا بدأت صراحة مكالمتك coroutine غير المتزامنة مع SupervisorJob
. وبالتالي ، فإن الكود أدناه سيظل يتعطل تطبيقك ، لأن async
تعمل كجزء من coroutine الأصل (1).
val job = SupervisorJob() val scope = CoroutineScope(Dispatchers.Default + job) fun loadData() = scope.launch { try { async {
هناك طريقة أخرى لتجنب حدوث تعطل ، وهو الأفضل ، وهي التفاف async
في coroutineScope
(1). الآن ، عندما يحدث استثناء داخل async
، فإنه يلغي جميع coroutines الأخرى التي تم إنشاؤها في هذه المنطقة ، دون لمس المنطقة الخارجية. (2)
val job = SupervisorJob() val scope = CoroutineScope(Dispatchers.Default + job)
بالإضافة إلى ذلك ، يمكنك معالجة الاستثناءات داخل كتلة async
.
استخدام المدير الرئيسي ل coroutines الجذر
إذا كنت بحاجة إلى عمل الخلفية وتحديث واجهة المستخدم داخل coroutine الجذر الخاص بك ، ابدأ تشغيله باستخدام المرسل الرئيسي.
val scope = CoroutineScope(Dispatchers.Default)
في المثال أعلاه ، نبدأ تشغيل coroutine الجذر باستخدام المرسل CoroutineScope
في CoroutineScope
(1). مع هذا النهج ، في كل مرة نحتاج فيها إلى تحديث واجهة المستخدم ، سيتعين علينا تبديل السياق (2).
في معظم الحالات ، من الأفضل إنشاء CoroutineScope
الفور باستخدام المرسل الرئيسي ، مما سيؤدي إلى تبسيط التعليمات البرمجية وتبديل السياق بشكل أقل وضوحًا.
val scope = CoroutineScope(Dispatchers.Main) fun login() = scope.launch { view.showLoading() withContext(Dispatcher.IO) { networkClient.login(...) } view.hideLoading() }
تجنب استخدام المزامنة / الانتظار غير الضرورية
إذا كنت تستخدم وظيفة async
الفور await
، فيجب عليك التوقف عن ذلك.
launch { val data = async(Dispatchers.Default) { }.await() }
إذا كنت ترغب في تبديل سياق coroutine وتعليق coroutine الأصل على الفور ، فإن withContext
هي الطريقة الأفضل لهذا الغرض.
launch { val data = withContext(Dispatchers.Default) { } }
فيما يتعلق بالأداء ، ليست هذه مشكلة كبيرة (حتى لو نظرنا إلى أن async
تخلق coroutine جديد لهذه المهمة) ، ولكن async
الدلالي يعني أنك تريد تشغيل العديد من coroutines في الخلفية ثم تنتظرها فقط.
تجنب إلغاء الوظيفة
إذا كنت بحاجة إلى إلغاء coroutine ، لا تلغي المهمة.
class WorkManager { val job = SupervisorJob() val scope = CoroutineScope(Dispatchers.Default + job) fun doWork1() { scope.launch { } } fun doWork2() { scope.launch { } } fun cancelAllWork() { job.cancel() } } fun main() { val workManager = WorkManager() workManager.doWork1() workManager.doWork2() workManager.cancelAllWork() workManager.doWork1()
المشكلة في الكود أعلاه هي أنه عندما نقوم بإلغاء المهمة ، نضعها في حالة مكتملة . لن يتم تنفيذ Coroutines التي تم إطلاقها كجزء من المهمة المكتملة (1).
إذا كنت تريد التراجع عن جميع coroutines في منطقة معينة ، يمكنك استخدام وظيفة cancelChildren
. بالإضافة إلى ذلك ، من الممارسات الجيدة توفير القدرة على إلغاء الوظائف الفردية (2).
class WorkManager { val job = SupervisorJob() val scope = CoroutineScope(Dispatchers.Default + job) fun doWork1(): Job = scope.launch { }
تجنب كتابة وظيفة الإيقاف المؤقت باستخدام المرسل الضمني
لا تكتب وظيفة suspend
، والتي يعتمد تنفيذها على مدير coroutine المحدد.
suspend fun login(): Result { view.showLoading() val result = withContext(Dispatcher.IO) { someBlockingCall() } view.hideLoading() return result }
في المثال أعلاه ، تكون وظيفة تسجيل الدخول هي وظيفة تعليق وستفشل إذا قمت ببدء تشغيلها من coroutine لن يستخدمها المرسل الرئيسي.
launch(Dispatcher.Main) {
CalledFromWrongThreadException: يمكن فقط الوصول إلى مؤشر الترابط المصدر الذي أنشأ التسلسل الهرمي لمكونات عرض.
إنشاء وظيفة التعليق الخاصة بك بحيث يمكن تنفيذها من أي مدير coroutine.
suspend fun login(): Result = withContext(Dispatcher.Main) { view.showLoading() val result = withContext(Dispatcher.IO) { someBlockingCall() } view.hideLoading() return result }
الآن يمكننا استدعاء وظيفة تسجيل الدخول الخاصة بنا من أي مرسل.
launch(Dispatcher.Main) {
تجنب استخدام النطاق العالمي
إذا كنت تستخدم GlobalScope
في كل مكان في تطبيق Android الخاص بك ، فيجب أن تتوقف عن القيام بذلك.
GlobalScope.launch {
يتم استخدام النطاق العالمي لإطلاق coroutines المستوى الأعلى التي تعمل طوال عمر التطبيق ولا يتم إلغاؤها في وقت مبكر.
يجب أن يستخدم رمز التطبيق عادةً CoroutineScope الخاص بالتطبيق ، لذا فإن استخدام المتزامن أو التشغيل في GlobalScope لا يشجعه كثيرًا.
في Android ، يمكن أن يقتصر coroutine بسهولة على دورة حياة نشاط أو تجزئة أو عرض أو ViewModel.
class MainActivity : AppCompatActivity(), CoroutineScope { private val job = SupervisorJob() override val coroutineContext: CoroutineContext get() = Dispatchers.Main + job override fun onDestroy() { super.onDestroy() coroutineContext.cancelChildren() } fun loadData() = launch {