كيفية استخدام كوروتينات في الطعام والنوم بسلام في الليل

Coroutines هي أداة قوية لتنفيذ التعليمات البرمجية غير المتزامنة. يعملون بالتوازي ، ويتواصلون مع بعضهم البعض ويستهلكون القليل من الموارد. يبدو أنه من دون خوف ، يمكن إدخال كوروتينات في الإنتاج. ولكن هناك مخاوف وتتدخل.

تقرير فلاديمير إيفانوف حول AppsConf يدور حول حقيقة أن الشيطان ليس فظيعًا للغاية وأنه يمكنك استخدام coroutines الآن:



عن المتحدث : Vladimir Ivanov ( dzigoro ) هو أحد مطوري Android الرائدين في EPAM ولديه 7 سنوات من الخبرة ، وهو مغرم بهندسة الحلول ، وتطوير React Native و iOS ، ولديه أيضًا شهادة Google Cloud Architect .

كل ما تقرأه هو نتاج إنتاج الخبرة والدراسات المختلفة ، لذا خذها كما هي دون أي ضمان.

Coroutines ، Kotlin و RxJava


للحصول على معلومات: الوضع الحالي للكوروتين في الإصدار ، غادر بيتا. تم إصدار Kotlin 1.3 ، وتم إعلان أن المستقرات مستقرة وهناك سلام في العالم.



لقد أجريت مؤخرًا استطلاعًا على Twitter مفاده أن الأشخاص الذين يستخدمون coroutine:

  • 13٪ كوروتينات في الغذاء. كل شيء على ما يرام ؛
  • 25٪ حاولوا تجربتهم في مشروع الحيوانات الأليفة.
  • 24٪ - ما هو كوتلن؟
  • الجزء الأكبر من 38٪ RxJava موجود في كل مكان.

الإحصائيات ليست سعيدة. أعتقد أن RxJava أداة معقدة للغاية للمهام التي يشيع استخدامها من قبل المطورين. Coroutines هي أكثر ملاءمة للسيطرة على عملية غير متزامنة.

في تقاريري السابقة ، تحدثت عن كيفية إعادة البناء من RxJava إلى Coroutines في Kotlin ، لذلك لن أتطرق إلى هذا بالتفصيل ، ولكن أذكر فقط النقاط الرئيسية.

لماذا نستخدم coroutines؟


لأنه إذا استخدمنا RxJava ، فستبدو أمثلة التنفيذ المعتادة كما يلي:

interface ApiClientRx { fun login(auth: Authorization) : Single<GithubUser> fun getRepositories (reposUrl: String, auth: Authorization) : Single<List<GithubRepository>> } //RxJava 2 implementation 

لدينا واجهة ، على سبيل المثال ، نكتب عميل GitHub ونريد إجراء عمليتين له:

  1. مستخدم تسجيل الدخول.
  2. احصل على قائمة بمستودعات GitHub.

في كلتا الحالتين ، ستُرجع الدالات كائنات العمل المنفردة: GitHubUser أو قائمة GitHubRepository.

رمز التنفيذ لهذه الواجهة هو كما يلي:

 private fun attemptLoginRx () { showProgress(true) compositeDisposable.add(apiClient.login(auth) .flatMap { user -> apiClient.getRepositories(user.repos_url, auth) } .map { list -> list.map { it.full_name } } .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .doFinally { showProgress(false) } .subscribe( { list -> showRepositories(this, list) }, { error -> Log.e("TAG", "Failed to show repos", error) } )) } 

- نأخذ مركب مركب يمكن التخلص منه حتى لا يكون هناك تسرب للذاكرة.
- إضافة مكالمة إلى الطريقة الأولى.
- نستخدم عوامل تشغيل مناسبة للحصول على المستخدم ، على سبيل المثال flatMap .
- نحصل على قائمة مستودعاتها.
- نكتب Boilerplate بحيث تعمل على الخيوط الصحيحة.
- عندما يكون كل شيء جاهزًا ، نعرض قائمة المستودعات الخاصة بالمستخدم الذي قام بتسجيل الدخول.

صعوبات رمز RxJava:

  • التعقيد في رأيي ، فإن الرمز معقد للغاية بالنسبة للمهمة البسيطة المتمثلة في مكالمتين شبكيتين وعرض شيء ما على واجهة المستخدم .
  • تتبعات مكدس غير منضم. لا ترتبط آثار المكدس تقريبًا بالكود الذي تكتبه.
  • الموارد الزائدة . يولد RxJava الكثير من الأشياء تحت غطاء المحرك وقد ينخفض ​​الأداء.

ماذا سيكون الرمز نفسه مع coroutines حتى الإصدار 0.26؟

في 0.26 ، تغيرت واجهة برمجة التطبيقات ، ونحن نتحدث عن الإنتاج. لم ينجح أحد حتى الآن في تطبيق 0.26 في حث ، ولكننا نعمل على ذلك.

مع coroutines ، ستتغير واجهتنا بشكل كبير . سوف تتوقف الدالات عن إرجاع أي أغراض فردية أو كائنات مساعدة أخرى. سيقومون بإرجاع عناصر الأعمال على الفور: GitHubUser وقائمة GitHubRepository. ستكون الدالتان GitHubUser و GitHubRepository تحتوي على مُعدِّلات تعليق . هذا أمر جيد ، لأن التعليق لا يلزمنا بأي شيء تقريبًا:

 interface ApiClient { suspend fun login(auth: Authorization) : GithubUser suspend fun getRepositories (reposUrl: String, auth: Authorization) : List<GithubRepository> } //Base interface 

إذا نظرت إلى الرمز الذي يستخدم تطبيق هذه الواجهة بالفعل ، فسوف يتغير بشكل كبير مقارنةً بـ RxJava:

 private fun attemptLogin () { launch(UI) { val auth = BasicAuthorization(login, pass) try { showProgress(true) val userlnfo = async { apiClient.login(auth) }.await() val repoUrl = userlnfo.repos_url val list = async { apiClient.getRepositories(repoUrl, auth) }.await() showRepositories( this, list.map { it -> it.full_name } ) } catch (e: RuntimeException) { showToast("Oops!") } finally { showProgress(false) } } } 

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

يبدو كل شيء كما لو أنه لا يوجد عمل غير متزامن يحدث ، ونكتب فقط الأوامر في العمود ويتم تنفيذها. في النهاية ، نقوم بما يجب القيام به في واجهة المستخدم.

لماذا هي coroutines أفضل؟

  • هذا الرمز أسهل للقراءة. مكتوب كما لو كان متناسقًا.
  • على الأرجح أن أداء هذا الرمز أفضل من RxJava.
  • من السهل جدًا كتابة الاختبارات ، ولكننا سنصل إليها بعد قليل.

خطوتين إلى الجانب


دعونا نتطرق قليلاً ، هناك شيئان لا يزالان بحاجة إلى المناقشة.

الخطوة 1. withContext vs launch / async


بالإضافة إلى منشئ Coroutine غير المتزامن ، هناك منشئ coroutine مع المحتوى .

إطلاق أو غير متزامن إنشاء سياق Coroutine جديد ، وهو ليس ضروريًا دائمًا. إذا كان لديك سياق Coroutine الذي تريد استخدامه خلال التطبيق ، فلن تحتاج إلى إعادة إنشائه. يمكنك ببساطة إعادة استخدام واحد موجود. للقيام بذلك ، ستحتاج إلى منشئ coroutine مع Context. إنه ببساطة يعيد استخدام سياق Coroutine الحالي. سيكون أسرع من مرتين إلى ثلاث مرات ، ولكنه الآن سؤال غير مبدئي. إذا كانت الأرقام الدقيقة مثيرة للاهتمام ، فإليك السؤال حول تدفق مكدس مع المعايير والتفاصيل.

قاعدة عامة: استخدم withContext بدون أدنى شك حيث يناسب دلالة. ولكن إذا كنت بحاجة إلى تحميل متوازي ، على سبيل المثال عدة صور أو أجزاء من البيانات ، فإن خيار المزامنة / الانتظار هو اختيارك.

الخطوة 2. إعادة بيع ديون


ماذا لو قمت بإعادة بناء سلسلة RxJava معقدة حقًا؟ لقد صادفت هذا في الإنتاج:

 observable1.getSubject().zipWith(observable2.getSubject(), (t1, t2) -> { // side effects return true; }).doOnError { // handle errors } .zipWith(observable3.getSubject(), (t3, t4) -> { // side effects return true; }).doOnComplete { // gather data } .subscribe() 

كان لدي سلسلة معقدة مع موضوع عام ، مع الرمز البريدي والآثار الجانبية في كل سحاب ترسل شيئًا آخر إلى حافلة الحدث. كانت المهمة على الأقل للتخلص من حافلة الحدث. جلست ليوم واحد ، ولكن لم أستطع إعادة صياغة الرمز لحل المشكلة. اتضح القرار الصحيح لرمي كل شيء وإعادة كتابة الرمز على coroutine في 4 ساعات .

الرمز أدناه مشابه جدًا لما حصلت عليه:

 try { val firstChunkJob = async { call1 } val secondChunkJob = async { call2 } val thirdChunkJob = async { call3 } return Result( firstChunkJob.await(), secondChunkJob.await(), thirdChunkJob.await()) } catch (e: Exception) { // handle errors } 

- نقوم بمزامنة مهمة واحدة للمهمة الثانية والثالثة.
- نحن ننتظر النتيجة ونضع كل شيء في شيء.
- انتهى!

إذا كان لديك سلاسل معقدة وهناك coroutines ، ثم مجرد إعادة بناء. إنه سريع حقًا.

ما الذي يمنع المطورين من استخدام coroutines في همز؟


في رأيي ، نحن كمطورين ، ممنوعون حاليًا من استخدام coroutines فقط بسبب الخوف من شيء جديد:

  • لا نعرف ماذا نفعل بدورة الحياة والنشاط ودورة حياة الشظايا. كيف تعمل مع coroutines في هذه الحالات؟
  • لا توجد خبرة في حل المهام المعقدة اليومية في الإنتاج باستخدام كوروتين.
  • لا توجد أدوات كافية. تم كتابة مجموعة من المكتبات والوظائف لـ RxJava. على سبيل المثال RxFCM . RxJava نفسها لديها الكثير من العوامل ، وهو أمر جيد ، ولكن ماذا عن Coroutine؟
  • نحن لا نفهم حقا كيفية اختبار coroutines.

إذا تخلصنا من هذه المخاوف الأربعة ، فيمكننا النوم بهدوء في الليل واستخدام coroutines في الإنتاج.

دعونا نقطة نقطة.

1. إدارة دورة الحياة


  • يمكن أن تتسرب Coroutines كما يمكن التخلص منها أو AsyncTask . يجب حل هذه المشكلة يدويًا.
  • لتجنب استثناء مؤشر Null العشوائي ، يجب إيقاف coroutines.

توقف


هل أنت معتاد على Thread.stop () ؟ إذا استخدمته ، فليس لفترة طويلة. في JDK 1.1 ، تم الإعلان عن الطريقة على الفور أنها عفا عليها الزمن ، لأنه من المستحيل أخذ وإيقاف جزء معين من التعليمات البرمجية ولا توجد ضمانات بأنها ستكمل بشكل صحيح. على الأرجح سوف تحصل على تلف الذاكرة فقط.

لذلك ، لا يعمل Thread.stop () . تحتاج إلى الإلغاء لتكون تعاونية ، أي الرمز الموجود على الجانب الآخر لتعلم أنك تقوم بإلغائه.

كيف نطبق التوقفات مع RxJava:

 private val compositeDisposable = CompositeDisposable() fun requestSmth() { compositeDisposable.add( apiClientRx.requestSomething() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(result -> {}) } override fun onDestroy() { compositeDisposable.dispose() } 


في RxJava نستخدم CompositeDisposable .

- إضافة المتغير المركب القابل للتصرف إلى النشاط في الجزء أو في العارض ، حيث نستخدم RxJava.
- في onDestro y add Dispose وتختفي جميع الاستثناءات من تلقاء نفسها.

تقريبا نفس المبدأ مع coroutines:

 private val job: Job? = null fun requestSmth() { job = launch(UI) { val user = apiClient.requestSomething() … } } override fun onDestroy() { job?.cancel() } 

فكر في مثال لمهمة بسيطة .

عادة ، يعيد بناة Coroutine وظيفة ، وفي بعض الحالات مؤجلة .

- يمكننا حفظ هذه الوظيفة.
- إعطاء الأمر "إطلاق" باني coroutine . تبدأ العملية ، يحدث شيء ، يتم تذكر نتيجة التنفيذ.
- إذا لم ننقل أي شيء آخر ، فإن "الإطلاق" يبدأ الوظيفة ويعيد إلينا رابطًا إلى الوظيفة.
- يتم تذكر الوظيفة ، وفي onDestroy نقول "إلغاء" وكل شيء يعمل بشكل جيد.

ما هي مشكلة النهج؟ تحتاج كل وظيفة إلى حقل. تحتاج إلى الاحتفاظ بقائمة من الوظائف لإلغاءها جميعًا معًا. النهج يؤدي إلى تكرار التعليمات البرمجية ، لا تفعل ذلك.

الخبر السار هو أن لدينا بدائل : CompositeJob ووظيفة Lifecycle .

CompositeJob هو تناظري من CompositeDisposable. يبدو شيء مثل هذا :

 private val job: CompositeJob = CompositeJob() fun requestSmth() { job.add(launch(UI) { val user = apiClient.requestSomething() ... }) } override fun onDestroy() { job.cancel() } 

- لجزء واحد نبدأ وظيفة واحدة.
- نضع جميع الوظائف في CompositeJob ونعطي الأمر: "job.cancel () للجميع!" .

يتم تنفيذ النهج بسهولة في 4 أسطر ، دون احتساب إعلان الفئة:

 Class CompositeJob { private val map = hashMapOf<String, Job>() fun add(job: Job, key: String = job.hashCode().toString()) = map.put(key, job)?.cancel() fun cancel(key: String) = map[key]?.cancel() fun cancel() = map.forEach { _ ,u -> u.cancel() } } 


ستحتاج:

- خريطة بمفتاح سلسلة ،
- إضافة طريقة ، حيث ستضيف وظيفة ،
- معلمة مفتاح اختيارية.

إذا كنت ترغب في استخدام نفس المفتاح لنفس الوظيفة - من فضلك. إذا لم يكن كذلك ، فإن hashCode سيحل مشكلتنا. أضف الوظيفة إلى الخريطة التي مررنا بها وألغِ المهمة السابقة بنفس المفتاح. إذا تجاوزنا المهمة ، فإن النتيجة السابقة لا تهمنا. نلغيها ونقودها مرة أخرى.

الإلغاء بسيط: نحصل على الوظيفة عن طريق المفتاح وإلغاء. الإلغاء الثاني للخريطة بالكامل يلغي كل شيء. كل رمز مكتوب في نصف ساعة في أربعة أسطر ويعمل. إذا كنت لا ترغب في الكتابة ، خذ المثال أعلاه.

وظيفة تدرك دورة الحياة


هل استخدمت Android Lifecycle أو مالك Lifecycle أو مراقب ؟


نشاطنا وأجزائه لها حالات معينة. يسلط الضوء على: إنشاء وبدء واستئناف . هناك انتقالات مختلفة بين الدول. يسمح لك LifecycleObserver بالاشتراك في هذه التحولات والقيام بشيء عند حدوث أحد التحولات.

تبدو بسيطة للغاية:

 public class MyObserver implements LifecycleObserver { @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) public void connectListener() { ... } @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) public void disconnectListener() { … } } 

يمكنك تعليق التعليق التوضيحي باستخدام بعض المعلمات على الطريقة ، ويتم استدعاؤه مع الانتقال المقابل. ببساطة استخدم هذا النهج للكوروتين:

 class AndroidJob(lifecycle: Lifecycle) : Job by Job(), LifecycleObserver { init { lifecycle.addObserver(this) } @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) fun destroy() { Log.d("AndroidJob", "Cancelling a coroutine") cancel() } } 

- يمكنك كتابة AndroidJob فئة أساسية .
- سننقل دورة الحياة إلى الفصل.
- ستقوم واجهة LifecycleObserver بتنفيذ المهمة.

كل ما نحتاجه:

- في المنشئ أضف إلى دورة الحياة كمراقب.
- اشترك في ON_DESTROY أو أي شيء آخر يهمنا.
- إلغاء الأمر في ON_DESTROY.
- احصل على أحد الوالدين في الجزء الخاص بك.
- اتصل بوظائف منشئ الفرح أو دورة حياة جزء نشاطك. لا فرق.
- تمرير هذا الوالد كوالد .

يبدو الرمز النهائي كما يلي:

 private var parentJob = AndroidJob(lifecycle) fun do() { job = launch(UI, parent = parentJob) { // code } } 

عندما تلغي أحد الوالدين ، يتم إلغاء جميع اللقاحات الفرعية للأطفال ولا تحتاج بعد الآن إلى كتابة أي شيء في الجزء. كل شيء يحدث تلقائيًا ، ليس بعد ON_DESTROY. الشيء الرئيسي لا تنسى تمرير الوالدين = الوالدين .

إذا كنت تستخدم ، يمكنك كتابة قاعدة الوبر البسيطة التي ستبرزك: "أوه ، لقد نسيت والديك!"

مع   تم فرز إدارة دورة الحياة. لدينا زوجان من الأدوات التي تتيح لك القيام بذلك بسهولة وراحة.

ماذا عن السيناريوهات المعقدة والمهام غير التافهة في الإنتاج؟

2. حالات الاستخدام المعقدة


السيناريوهات المعقدة والمهام غير التافهة هي:

- عوامل التشغيل - عوامل التشغيل المعقدة في RxJava: flatMap و debounce وما إلى ذلك.
- معالجة الأخطاء - معالجة الأخطاء المعقدة. ليس فقط حاول .. الصيد ، ولكن متداخلة على سبيل المثال.
- التخزين المؤقت مهمة غير تافهة. في الإنتاج ، واجهنا ذاكرة تخزين مؤقت وأردنا الحصول على أداة لحل مشكلة التخزين المؤقت مع coroutines بسهولة.

كرر


عندما فكرنا في عوامل تشغيل coroutine ، كان الخيار الأول هو تكرار عندما () .

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

مع coroutines ، يتم تنفيذ هذه المهمة بسهولة:

 suspend fun <T> retryDeferredWithDelay( deferred: () -> Deferred<T>, tries: Int = 3, timeDelay: Long = 1000L ): T { for (i in 1..tries) { try { return deferred().await() } catch (e: Exception) { if (i < tries) delay(timeDelay) else throw e } } throw UnsupportedOperationException() } 


تنفيذ المشغل:

- يأخذ مؤجل .
- ستحتاج إلى استدعاء غير متزامن للحصول على هذا الكائن.
- بدلاً من المؤجل ، يمكنك تمرير كل من كتلة التعليق وأي وظيفة تعليق بشكل عام.
- حلقة for - أنت تنتظر نتيجة كوروتين الخاص بك. إذا حدث شيء ولم يتم استنفاد عداد التكرار ، فحاول مرة أخرى من خلال Delay . إذا لم يكن كذلك ، فلا.

يمكن تخصيص الوظيفة بسهولة: ضع تأخيرًا أسيًا أو قم بتمرير وظيفة لامدا التي ستحسب التأخير اعتمادًا على الظروف.

استخدمها ، إنها تعمل!

الكود البريدية


كثيرا ما نواجههم. هنا مرة أخرى ، كل شيء بسيط:

 suspend fun <T1, T2, R> zip( source1: Deferred<T1>, source2: Deferred<T2>, zipper: BiFunction<T1, T2, R>): R { return zipper.apply(sourcel.await(), source2.await()) } suspend fun <T1, T2, R> Deferred<T1>.zipWith( other: Deferred<T2>, zipper: BiFunction<T1, T2, R>): R { return zip(this, other, zipper) } 

- استخدم السوستة والدعوة تنتظر على مؤجل الخاص بك.
- بدلاً من "مؤجل" ، يمكنك استخدام وظيفة الإيقاف ومنشئ coroutine مع withContext. سوف تنقل السياق الذي تحتاجه.

يعمل هذا مرة أخرى وآمل أن أزيل هذا الخوف.

مخبأ



هل لديك تنفيذ مخبأ في الإنتاج مع RxJava؟ نستخدم RxCache.


في الرسم البياني على اليسار: عرض و ViewModel . على اليمين توجد مصادر البيانات: مكالمات الشبكة وقاعدة البيانات.

إذا أردنا تخزين شيء ما في ذاكرة التخزين المؤقت ، فستكون ذاكرة التخزين المؤقت مصدرًا آخر للبيانات.

أنواع ذاكرة التخزين المؤقت:

  • مصدر الشبكة لمكالمات الشبكة.
  • ذاكرة التخزين المؤقت في الذاكرة .
  • ذاكرة تخزين مؤقت مستمرة مع انتهاء الصلاحية ليتم تخزينها على القرص بحيث تبقى ذاكرة التخزين المؤقت سارية بعد إعادة تشغيل التطبيق.

لنكتب ذاكرة تخزين مؤقت بسيطة وبدائية للحالة الثالثة. يأتي منشئ Coroutine مع Context إلى الإنقاذ مرة أخرى.

 launch(UI) { var data = withContext(dispatcher) { persistence.getData() } if (data == null) { data = withContext(dispatcher) { memory.getData() } if (data == null) { data = withContext(dispatcher) { network.getData() } memory.cache(url, data) persistence.cache(url, data) } } } 

- يمكنك تنفيذ كل عملية باستخدام withContext ومعرفة ما إذا كانت هناك أي بيانات قادمة.
- إذا لم تأت البيانات من الإصرار ، فأنت تحاول الحصول عليها من ذاكرة التخزين المؤقت .
- إذا لم يكن هناك ذاكرة تخزين مؤقت أيضًا ، فاتصل بمصدر الشبكة واحصل على بياناتك. لا تنسى بالطبع وضع كل المخابئ.

هذا تنفيذ بدائي إلى حد ما وهناك العديد من الأسئلة ، لكن الطريقة تعمل إذا كنت بحاجة إلى ذاكرة تخزين مؤقت في مكان واحد. بالنسبة لمهام الإنتاج ، ذاكرة التخزين المؤقت هذه ليست كافية. هناك حاجة إلى شيء أكثر تعقيدًا.

Rx لديه RxCache


بالنسبة لأولئك الذين لا يزالون يستخدمون RxJava ، يمكنك استخدام RxCache. ما زلنا نستخدمها أيضا. RxCache هي مكتبة خاصة. يتيح لك تخزين البيانات مؤقتًا وإدارة دورة حياته.

على سبيل المثال ، تريد أن تقول أن هذه البيانات ستنتهي صلاحيتها بعد 15 دقيقة: "من فضلك ، بعد هذه الفترة الزمنية ، لا ترسل بيانات من ذاكرة التخزين المؤقت ، ولكن أرسل لي بيانات جديدة."

المكتبة رائعة من حيث أنها تدعم الفريق بشكل معلن. الإعلان مشابه جدًا لما تفعله مع التعديل التحديثي :

 public interface FeatureConfigCacheProvider { @ProviderKey("features") @LifeCache(duration = 15, timeUnit = TimeUnit.MINUTES) fun getFeatures( result: Observable<Features>, cacheName: DynamicKey ): Observable<Reply<Features>> } 

- أنت تقول أن لديك CacheProvider .
- ابدأ طريقة وقول أن عمر LifeCache هو 15 دقيقة. المفتاح الذي سيكون متاحًا به هو الميزات .
- إرجاع <ملحوظ ، رد ، حيث يعد الرد كائن مكتبة مساعدة للعمل مع ذاكرة التخزين المؤقت.

الاستخدام بسيط للغاية:

 val restObservable = configServiceRestApi.getFeatures() val features = featureConfigCacheProvider.getFeatures( restObservable, DynamicKey(CACHE_KEY) ) 

- من ذاكرة التخزين المؤقت Rx ، قم بالوصول إلى RestApi .
- أنتقل إلى CacheProvider .
- اطعمه ملاحظة.
- ستكتشف المكتبة نفسها ما يجب القيام به: اذهب إلى ذاكرة التخزين المؤقت أم لا ، إذا نفد الوقت ، فانتقل إلى الملاحظة ويمكن إجراء عملية أخرى.

استخدام المكتبة مريح للغاية وأود الحصول على مكتبة مماثلة للكوروتين.

مخبأ Coroutine في التنمية


داخل EPAM ، نكتب مكتبة Coroutine Cache ، والتي ستؤدي جميع وظائف RxCache. قمنا بكتابة النسخة الأولى وتشغيلها داخل الشركة. بمجرد إصدار الإصدار الأول ، سأكون سعيدًا لنشره على Twitter الخاص بي. سيبدو هذا:

 val restFunction = configServiceRestApi.getFeatures() val features = withCache(CACHE_KEY) { restFunction() } 

سيكون لدينا وظيفة تعليق getFeatures . سننقل الوظيفة ككتلة إلى وظيفة خاصة عالية الترتيب مع ذاكرة التخزين المؤقت ، والتي ستحدد ما يجب القيام به.

ربما سنقوم بعمل نفس الواجهة لدعم الوظائف التعريفية.

معالجة الخطأ




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

في الحالات البسيطة ، يكون كل شيء بسيطًا بشكل متوقع - يتم التعامل مع الأخطاء مع coroutines من خلال try-catch-أخيرا .

في الإنتاج ، بالإضافة إلى الحالات البسيطة ، هناك:

- محاولة التقاط متداخلة ،
- العديد من أنواع الاستثناءات المختلفة ،
- أخطاء في الشبكة أو في منطق الأعمال ،
- أخطاء المستخدم. قام مرة أخرى بشيء خاطئ وكان المسؤول عن كل شيء.

يجب أن نكون مستعدين لهذا.

هناك حلان : CoroutineExceptionHandler والنهج مع فئات النتائج .

معالج استثناء Coroutine


هذه فئة خاصة للتعامل مع حالات الأخطاء المعقدة. يسمح لك ExceptionHandler بأخذ استثناءك كحجة كخطأ والتعامل معه.

كيف نتعامل عادة مع الأخطاء المعقدة؟

ضغط المستخدم على شيء ما ، ولم يعمل الزر. عليه أن يقول الخطأ الذي حدث وتوجيهه إلى إجراء محدد: تحقق من الإنترنت ، أو Wi-Fi ، أو حاول لاحقًا أو احذف التطبيق ولا تستخدمه مرة أخرى مطلقًا. يمكن أن يكون قول هذا للمستخدم بسيطًا جدًا:

 val handler = CoroutineExceptionHandler(handler = { , error -> hideProgressDialog() val defaultErrorMsg = "Something went wrong" val errorMsg = when (error) { is ConnectionException -> userFriendlyErrorMessage(error, defaultErrorMsg) is HttpResponseException -> userFriendlyErrorMessage(Endpoint.EndpointType.ENDPOINT_SYNCPLICITY, error) is EncodingException -> "Failed to decode data, please try again" else -> defaultErrorMsg } Toast.makeText(context, errorMsg, Toast.LENGTH_SHORT).show() }) 

- دعنا نحصل على الرسالة الافتراضية: "حدث خطأ ما!" وتحليل الاستثناء.
- إذا كان هذا هو ConnectionException ، فإننا نأخذ رسالة مترجمة من الموارد: "يا رجل ، قم بتشغيل Wi-Fi وستزول مشاكلك. أضمن ذلك. "
- إذا قال الخادم شيئًا خاطئًا ، فأنت بحاجة إلى إخبار العميل: "تسجيل الخروج وتسجيل الدخول مرة أخرى" ، أو "لا تفعل ذلك في موسكو ، قم بذلك في بلد آخر" ، أو "عذرًا ، الرفيق. كل ما يمكنني فعله هو فقط أن أقول أن شيئًا ما حدث خطأ. "
- إذا كان هذا خطأ مختلفًا تمامًا ، على سبيل المثال ، نفاد الذاكرة ، نقول: "حدث خطأ ما ، أنا آسف".
- يتم عرض جميع الرسائل.

ما تكتبه إلى CoroutineExceptionHandler سيتم تنفيذه على نفس المرسل حيث تقوم بتشغيل Coroutine. لذلك ، إذا أعطيت أمر "إطلاق" واجهة المستخدم ، فسيحدث كل شيء على واجهة المستخدم. لا تحتاج إلى إرسال منفصل ، وهو أمر مريح للغاية.

الاستخدام بسيط:

 launch(uiDispatcher + handler) { ... } 

هناك عامل زائد . في سياق Coroutine ، أضف معالجًا ويعمل كل شيء ، وهو أمر مريح للغاية. استخدمنا هذا لفترة.

دروس النتيجة


أدركنا لاحقًا أن CoroutineExceptionHandler قد يكون مفقودًا. يمكن أن تتكون النتيجة ، التي تتكون من عمل coroutine ، من عدة بيانات ، من أجزاء مختلفة أو معالجة العديد من المواقف.

يساعد منهج فئات النتائج على التعامل مع هذه المشكلة:

 sealed class Result { data class Success(val payload: String) : Result() data class Error(val exception: Exception) : Result() } 

- في منطق عملك ، تبدأ فئة النتيجة .
- وضع علامة مختومة .
- ترث من فئتي البيانات الأخريين في الفئة: النجاح والخطأ .
Success , .
Error exception.

- :

 override suspend fun doTask(): Result = withContext(CommonPool) { if ( !isSessionValidForTask() ) { return@withContext Result.Error(Exception()) } … try { Result.Success(restApi.call()) } catch (e: Exception) { Result.Error(e) } } 

Coroutine context — Coroutine builder withContex .

, :

— , error. .
— RestApi -.
— , Result.Success .
— , Result.Error .

- , ExceptionHandler .

Result classes , . Result classes, ExceptionHandler try-catch.

3.


, . unit- , , . unit-.

, . , unit-, 2 :

  1. Replacing context . , ;
  2. Mocking coroutines . .

Replacing context


presenter:

 val login() { launch(UI) { … } } 

, login , UI-. , , . , , unit-.

:

 val login (val coroutineContext = UI) { launch(coroutineContext) { ... } } 

— login coroutineContext. , . Kotlin , UI .
— Coroutine builder Coroutine Contex, .

unit- :

 fun testLogin() { val presenter = LoginPresenter () presenter.login(Unconfined) } 


LoginPresenter login - , , Unconfined.
Unconfined , , . .

Mocking coroutines


— . Mockk unit-. unit- Kotlin, . suspend- coEvery -.

login githubUser :

 coEvery { apiClient.login(any()) } returns githubUser 

Mockito-kotlin , — . , , :

 given { runBlocking { apiClient.login(any()) } }.willReturn (githubUser) 

runBlocking . given- , .

Presenter :

 fun testLogin() { val githubUser = GithubUser('login') val presenter = LoginPresenter(mockApi) presenter.login (Unconfined) assertEquals(githubUser, presenter.user()) } 

— -, , GitHubUser .
— LoginPresenter API, . .
presenter.login Unconfined , Presenter , .

هذا كل ما في الأمر! .




  • Rx- . . , RxJava RxJava. - — , .
  • . , . Unit- — , , , . — welcome!
  • . , , , , . .


روابط مفيدة



أخبار

30 Mail.ru . , .

AppsConf , .

, , , .

youtube- AppsConf 2018 — :)

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


All Articles