أنماط Corutin ومضادات في Kotlin

أنماط Corutin ومضادات في Kotlin


قررت أن أكتب عن بعض الأشياء التي ، في رأيي ، يجب تجنبها عند استخدام Kotlin coroutine.


لف المكالمات غير المتزامنة في coroutineScope أو استخدم SupervisorJob للتعامل مع الاستثناءات


إذا حدث استثناء في كتلة async ، فلا تعتمد على try/catch .


 val job: Job = Job() val scope = CoroutineScope(Dispatchers.Default + job) // may throw Exception fun doWork(): Deferred<String> = scope.async { ... } // (1) fun loadData() = scope.launch { try { doWork().await() // (2) } catch (e: Exception) { ... } } 

في المثال أعلاه ، تبدأ وظيفة doWork coroutine (1) جديدة ، والتي قد تتسبب في استثناء غير معالَج. إذا حاولت التفاف doWork باستخدام doWork try/catch (2) ، فسيظل التطبيق يتعطل.


وذلك لأن فشل أي عنصر تابع في الوظيفة يؤدي إلى الفشل الفوري لوالدها.


طريقة واحدة لتجنب الخطأ هي استخدام SupervisorJob (1).


لن يؤدي فشل أو إلغاء تنفيذ المكون الفرعي إلى فشل الوالد ولن يؤثر على المكونات الأخرى.

 val job = SupervisorJob() // (1) val scope = CoroutineScope(Dispatchers.Default + job) // may throw Exception fun doWork(): Deferred<String> = scope.async { ... } fun loadData() = scope.launch { try { doWork().await() } catch (e: Exception) { ... } } 

ملاحظة : لن يعمل هذا إلا إذا بدأت صراحة مكالمتك coroutine غير المتزامنة مع SupervisorJob . وبالتالي ، فإن الكود أدناه سيظل يتعطل تطبيقك ، لأن async تعمل كجزء من coroutine الأصل (1).


 val job = SupervisorJob() val scope = CoroutineScope(Dispatchers.Default + job) fun loadData() = scope.launch { try { async { // (1) // may throw Exception }.await() } catch (e: Exception) { ... } } 

هناك طريقة أخرى لتجنب حدوث تعطل ، وهو الأفضل ، وهي التفاف async في coroutineScope (1). الآن ، عندما يحدث استثناء داخل async ، فإنه يلغي جميع coroutines الأخرى التي تم إنشاؤها في هذه المنطقة ، دون لمس المنطقة الخارجية. (2)


 val job = SupervisorJob() val scope = CoroutineScope(Dispatchers.Default + job) // may throw Exception fun doWork(): Deferred<String> = coroutineScope { // (1) async { ... } } fun loadData() = scope.launch { // (2) try { doWork().await() } catch (e: Exception) { ... } } 

بالإضافة إلى ذلك ، يمكنك معالجة الاستثناءات داخل كتلة async .


استخدام المدير الرئيسي ل coroutines الجذر


إذا كنت بحاجة إلى عمل الخلفية وتحديث واجهة المستخدم داخل coroutine الجذر الخاص بك ، ابدأ تشغيله باستخدام المرسل الرئيسي.


 val scope = CoroutineScope(Dispatchers.Default) // (1) fun login() = scope.launch { withContext(Dispatcher.Main) { view.showLoading() } // (2) networkClient.login(...) withContext(Dispatcher.Main) { view.hideLoading() } // (2) } 

في المثال أعلاه ، نبدأ تشغيل 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) { /* code */ }.await() } 

إذا كنت ترغب في تبديل سياق coroutine وتعليق coroutine الأصل على الفور ، فإن withContext هي الطريقة الأفضل لهذا الغرض.


 launch { val data = withContext(Dispatchers.Default) { /* code */ } } 

فيما يتعلق بالأداء ، ليست هذه مشكلة كبيرة (حتى لو نظرنا إلى أن async تخلق coroutine جديد لهذه المهمة) ، ولكن async الدلالي يعني أنك تريد تشغيل العديد من coroutines في الخلفية ثم تنتظرها فقط.


تجنب إلغاء الوظيفة


إذا كنت بحاجة إلى إلغاء coroutine ، لا تلغي المهمة.


 class WorkManager { val job = SupervisorJob() val scope = CoroutineScope(Dispatchers.Default + job) fun doWork1() { scope.launch { /* do work */ } } fun doWork2() { scope.launch { /* do work */ } } fun cancelAllWork() { job.cancel() } } fun main() { val workManager = WorkManager() workManager.doWork1() workManager.doWork2() workManager.cancelAllWork() workManager.doWork1() // (1) } 

المشكلة في الكود أعلاه هي أنه عندما نقوم بإلغاء المهمة ، نضعها في حالة مكتملة . لن يتم تنفيذ Coroutines التي تم إطلاقها كجزء من المهمة المكتملة (1).


إذا كنت تريد التراجع عن جميع coroutines في منطقة معينة ، يمكنك استخدام وظيفة cancelChildren . بالإضافة إلى ذلك ، من الممارسات الجيدة توفير القدرة على إلغاء الوظائف الفردية (2).


 class WorkManager { val job = SupervisorJob() val scope = CoroutineScope(Dispatchers.Default + job) fun doWork1(): Job = scope.launch { /* do work */ } // (2) fun doWork2(): Job = scope.launch { /* do work */ } // (2) fun cancelAllWork() { scope.coroutineContext.cancelChildren() // (1) } } fun main() { val workManager = WorkManager() workManager.doWork1() workManager.doWork2() workManager.cancelAllWork() workManager.doWork1() } 

تجنب كتابة وظيفة الإيقاف المؤقت باستخدام المرسل الضمني


لا تكتب وظيفة suspend ، والتي يعتمد تنفيذها على مدير coroutine المحدد.


 suspend fun login(): Result { view.showLoading() val result = withContext(Dispatcher.IO) { someBlockingCall() } view.hideLoading() return result } 

في المثال أعلاه ، تكون وظيفة تسجيل الدخول هي وظيفة تعليق وستفشل إذا قمت ببدء تشغيلها من coroutine لن يستخدمها المرسل الرئيسي.


 launch(Dispatcher.Main) { // (1)    val loginResult = login() ... } launch(Dispatcher.Default) { // (2)   val loginResult = login() ... } 

CalledFromWrongThreadException: يمكن فقط الوصول إلى مؤشر الترابط المصدر الذي أنشأ التسلسل الهرمي لمكونات عرض.

إنشاء وظيفة التعليق الخاصة بك بحيث يمكن تنفيذها من أي مدير coroutine.


 suspend fun login(): Result = withContext(Dispatcher.Main) { view.showLoading() val result = withContext(Dispatcher.IO) { someBlockingCall() } view.hideLoading() return result } 

الآن يمكننا استدعاء وظيفة تسجيل الدخول الخاصة بنا من أي مرسل.


 launch(Dispatcher.Main) { // (1) no crash val loginResult = login() ... } launch(Dispatcher.Default) { // (2) no crash ether val loginResult = login() ... } 

تجنب استخدام النطاق العالمي


إذا كنت تستخدم GlobalScope في كل مكان في تطبيق Android الخاص بك ، فيجب أن تتوقف عن القيام بذلك.


 GlobalScope.launch { // code } 

يتم استخدام النطاق العالمي لإطلاق 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 { // code } } 

Source: https://habr.com/ru/post/ar432942/


All Articles