مرحبا يا هبر!
نذكرك أن لدينا بالفعل طلب 
مسبق للكتاب الذي طال انتظاره عن لغة Kotlin من سلسلة Big Nerd Ranch Guides الشهيرة. قررنا اليوم أن نلفت انتباهكم إلى ترجمة لمقال يخبرنا عن corotines Kotlin وعن العمل الصحيح مع تدفقات في Android. تتم مناقشة الموضوع بنشاط كبير ، وبالتالي ، للتأكد من اكتماله ، نوصي أيضًا بالاطلاع على 
هذه المقالة من Habr 
وهذه المشاركة المفصلة من مدونة برامج Axmor.
يعمل الإطار التنافسي الحديث في Java / Android على إلحاق الجحيم بالردود ويؤدي إلى حالات الحظر ، نظرًا لأن Android ليس لديه طريقة بسيطة إلى حد ما لضمان أمان سلاسل الرسائل.
corotines Kotlin هي مجموعة أدوات فعالة وكاملة للغاية مما يجعل إدارة المنافسة أسهل بكثير وأكثر إنتاجية.
وقفة وحظر: ما هو الفرقلا يحل Coroutines محل الخيوط ، ولكنه يوفر إطارًا لإدارتها. تتمثل فلسفة corutin في تحديد سياق يتيح لك 
الانتظار حتى تكتمل عمليات الخلفية دون حظر الخيط الرئيسي.
هدف Corutin في هذه الحالة هو الاستغناء عن عمليات الاسترجاعات وتبسيط المنافسة.
مثال أبسطبادئ ذي بدء ، دعنا نأخذ أبسط مثال: تشغيل coroutine في سياق 
Main (سلسلة رئيسية). في ذلك ، سنقوم باستخراج الصورة من دفق 
IO وإرسال هذه الصورة للمعالجة مرة أخرى إلى 
Main .
 launch(Dispatchers.Main) { val image = withContext(Dispatchers.IO) { getImage() }  
الكود بسيط كدالة مفردة الترابط. علاوة على ذلك ، في حين يتم تنفيذ 
getImage في المجموعة المخصصة من مؤشرات ترابط 
IO ، فإن الخيط الرئيسي مجاني ويمكنه القيام بأي مهمة أخرى! الدالة withContext توقف مؤقتًا coroutine الحالي أثناء تشغيل الإجراء الخاص به ( 
getImage() ). بمجرد إرجاع 
getImage() وتصبح 
looper من الخيط الرئيسي ، سيستأنف coroutine العمل في الخيط الرئيسي ويدعو 
imageView.setImageBitmap(image) .
المثال الثاني: الآن نحتاج إلى إنجاز مهمتين في الخلفية حتى يمكن استخدامهما. سنستخدم duet async / انتظار بحيث يتم تنفيذ هاتين المهمتين بالتوازي ، واستخدام نتيجتهما في سلسلة العمليات الرئيسية بمجرد أن تكون كلتا المهمتين جاهزة:
 val job = launch(Dispatchers.Main) { val deferred1 = async(Dispatchers.Default) { getFirstValue() } val deferred2 = async(Dispatchers.IO) { getSecondValue() } useValues(deferred1.await(), deferred2.await()) } job.join()  
async غير 
async مشابه 
launch ، ولكنه يعود 
deferred (كيان Kotlin مكافئ 
Future ) ، لذلك يمكن الحصول على النتيجة باستخدام 
await() . عند استدعاء بدون معلمات ، يعمل في السياق الافتراضي للنطاق الحالي.
مرة أخرى ، يبقى الخيط الرئيسي مجانيًا بينما ننتظر قيمتين.
كما ترى ، تقوم وظيفة 
launch بإرجاع 
Job ، والتي يمكن استخدامها للانتظار حتى تكتمل العملية - يتم ذلك باستخدام دالة 
join() . إنه يعمل كما هو الحال في أي لغة أخرى ، مع تحذير أنه ببساطة 
يعلق coroutine ، ولا يمنع التدفق .
إيفادإيفاد هو مفهوم رئيسي عند العمل مع coroutines. يتيح لك هذا الإجراء "الانتقال" من سلسلة رسائل إلى أخرى.
فكر في الشكل المكافئ للإرسال في 
Main في java ، أي ،
 runOnUiThread: public final void runOnUiThread(Runnable action) { if (Thread.currentThread() != mUiThread) { mHandler.post(action);  
تنفيذ السياق 
Main لنظام Android هو 
Handler قائم على 
Handler . لذلك هذا هو في الواقع تنفيذ مناسب للغاية:
 launch(Dispatchers.Main) { ... } vs launch(Dispatchers.Main, CoroutineStart.UNDISPATCHED) { ... }  
launch(Dispatchers.Main) يرسل 
Runnable إلى 
Handler ، لذلك لا يتم تنفيذ التعليمات البرمجية الخاصة به على الفور.
launch(Dispatchers.Main, CoroutineStart.UNDISPATCHED) سينفذ على الفور تعبير lambda في الخيط الحالي.
Dispatchers.Main يضمن أنه عندما يستأنف coroutine العمل ، سيتم توجيهه إلى الموضوع الرئيسي . بالإضافة إلى ذلك ، يتم استخدام Handler هنا كتطبيق Android أصلي لإرساله إلى حلقة حدث التطبيق.
التنفيذ الدقيق يشبه هذا:
 val Main: HandlerDispatcher = HandlerContext(mainHandler, "Main") 
إليك مقالة جيدة لمساعدتك في فهم تعقيدات الإرسال في Android:
فهم Android Core: Looper و Handler و HandlerThread .
السياق Coroutineيحدد سياق coroutine (المعروف أيضًا باسم مدير coroutine) في سلسلة الرسائل التي سيتم تنفيذ التعليمات البرمجية الخاصة بها ، وما الذي يجب فعله في حالة طرح استثناء ، ويشير إلى السياق الأصلي لنشر الإلغاء.
 val job = Job() val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable -> whatever(throwable) } launch(Disaptchers.Default+exceptionHandler+job) { ... } 
job.cancel() بإلغاء جميع coroutines الذي يكون الوالد هو 
job . سوف تحصل على استثناء هندلر جميع الاستثناءات التي ألقيت في هذه coroutines.
مجالتبسط واجهة 
coroutineScope معالجة الأخطاء:
إذا فشل أي من ابنتها coroutines ، ثم سيتم إلغاء النطاق بأكمله وجميع coroutines الطفل.
في المثال غير 
async ، إذا لم يكن من الممكن استخراج القيمة ، بينما استمرت مهمة أخرى في العمل ، فلدينا حالة تالفة ، وعلينا القيام بشيء حيال ذلك.
عند العمل مع 
coroutineScope ، لن يتم استدعاء الدالة 
useValues إلا في حالة نجاح استخراج كلا القيمتين. أيضا ، إذا فشل 
deferred1 ، سيتم إلغاء 
deferred1 .
 coroutineScope { val deferred1 = async(Dispatchers.Default) { getFirstValue() } val deferred2 = async(Dispatchers.IO) { getSecondValue() } useValues(deferred1.await(), deferred2.await()) } 
يمكنك أيضًا "وضع نطاق" فصلًا بأكمله لتعيين 
CoroutineContext افتراضيًا له واستخدامه.
فئة المثال التي تنفذ واجهة 
CoroutineScope :
 open class ScopedViewModel : ViewModel(), CoroutineScope { protected val job = Job() override val coroutineContext = Dispatchers.Main+job override fun onCleared() { super.onCleared() job.cancel() } } 
تشغيل Corutin في 
CoroutineScope :
التشغيل الافتراضي أو مدير 
async يصبح الآن مدير النطاق الحالي.
 launch { val foo = withContext(Dispatchers.IO) { … }  
الإطلاق المستقل ل coroutine (خارج أي CoroutineScope):
 GlobalScope.launch(Dispatchers.Main) {  
يمكنك حتى تحديد نطاق تطبيق ما عن طريق تعيين المرسل 
Main الافتراضي:
 object AppScope : CoroutineScope by GlobalScope { override val coroutineContext = Dispatchers.Main.immediate } 
تصريحات- Coroutines تحد التشغيل المتداخل مع جافا
- الحد من قابلية التحويل لتجنب الأقفال
- تم تصميم Coroutines للانتظار ، وليس لتنظيم المواضيع
- تجنب الإدخال / الإخراج في Dispatchers.Default(وMain...) - هذا هو ما Dispatchers.IO
- التدفقات تستهلك الموارد ، لذلك يتم استخدام السياقات ذات سلاسل الترابط المفردة
- يستند Dispatchers.DefaultإلىForkJoinPool، الذي تم تقديمه في Android 5+
- Coroutines يمكن استخدامها من خلال القنوات
التخلص من الأقفال وعمليات الاسترجاعات باستخدام القنواتتعريف القناة من وثائق JetBrains:
قناة Channel الناحية النظرية تشبه إلى حد بعيد BlockingQueue . يتمثل الاختلاف الرئيسي في أنه لا يحظر عملية البيع ، فهو يوفر send تعليق (أو عرض غير محظور) ، وبدلاً من حظر عملية receive ، فإنه ينص على receive تعليق receive .
الجهات الفاعلةالنظر في أداة بسيطة للعمل مع القنوات: 
Actor .
Actor ، مرة أخرى ، يشبه إلى حد كبير 
Handler : نحن نعرّف سياق coroutine (أي ، الخيط الذي سنقوم فيه بتنفيذ الإجراءات) ونعمل معه بترتيب متسلسل.
الفرق ، بالطبع ، هو أن كورينز تستخدم هنا ؛ 
يمكنك تحديد السلطة ، ورمز تنفيذها - توقف مؤقت .
من حيث المبدأ ، سيقوم 
actor بإعادة توجيه أي أمر إلى قناة coroutine. إنه 
يضمن تنفيذ أمر ويقيد العمليات في سياقها . يساعد هذا النهج تمامًا على التخلص من 
synchronize المكالمات والحفاظ على جميع المواضيع مجانية!
 protected val updateActor by lazy { actor<Update>(capacity = Channel.UNLIMITED) { for (update in channel) when (update) { Refresh -> updateList() is Filter -> filter.filter(update.query) is MediaUpdate -> updateItems(update.mediaList as List<T>) is MediaAddition -> addMedia(update.media as T) is MediaListAddition -> addMedia(update.mediaList as List<T>) is MediaRemoval -> removeMedia(update.media as T) } } }  
في هذا المثال ، نستخدم فئات Kotlin المختومة ، ونختار الإجراء الذي يجب القيام به.
 sealed class Update object Refresh : Update() class Filter(val query: String?) : Update() class MediaAddition(val media: Media) : Update() 
علاوة على ذلك ، سيتم وضع جميع هذه الإجراءات في قائمة الانتظار ، ولن يتم تنفيذها أبدًا بالتوازي. هذه طريقة ملائمة لتحقيق 
حدود التباين .
دورة حياة أندرويد + كوروتينيمكن أن تكون الجهات الفاعلة أيضًا مفيدة جدًا للتحكم في واجهة مستخدم Android ، وتبسيط إلغاء المهام ، ومنع التحميل الزائد للخيط الرئيسي.
دعنا ننفذ هذا وندعو 
job.cancel() عندما يتم تدمير النشاط.
 class MyActivity : AppCompatActivity(), CoroutineScope { protected val job = SupervisorJob()  
تشبه فئة 
SupervisorJob Job العادية باستثناء أن الإلغاء يمتد فقط في اتجاه المصب.
لذلك ، لا نقوم بإلغاء جميع coroutines في 
Activity عندما يفشل أحدهم.
الأمور أفضل قليلاً مع 
وظيفة ملحق تتيح لك الوصول إلى 
CoroutineContext من أي 
View في 
CoroutineScope .
 val View.coroutineContext: CoroutineContext? get() = (context as? CoroutineScope)?.coroutineContext 
الآن يمكننا الجمع بين كل هذا ، تقوم وظيفة 
setOnClick بإنشاء ممثل مشترك للتحكم في تصرفات 
onClick الخاصة به. في حالة الصنابير المتعددة ، سيتم تجاهل الإجراءات الوسيطة ، وبالتالي التخلص من أخطاء ANR (لا يستجيب التطبيق) ، وسيتم تنفيذ هذه الإجراءات في نطاق 
Activity . لذلك ، عندما يتم تدمير النشاط ، سيتم إلغاء كل هذا.
 fun View.setOnClick(action: suspend () -> Unit) {  
في هذا المثال ، قمنا بتعيين 
Channel على 
Conflated بحيث تتجاهل بعض الأحداث إذا كان هناك الكثير منها. يمكنك استبداله بـ 
Channel.UNLIMITED إذا كنت تفضل وضع قائمة انتظار الأحداث دون فقد أي منها ، ولكن لا تزال ترغب في حماية التطبيق من أخطاء ANR.
يمكنك أيضًا الجمع بين coroutines وإطارات Lifecycle لأتمتة إلغاء المهام المتعلقة بواجهة المستخدم:
 val LifecycleOwner.untilDestroy: Job get() { val job = Job() lifecycle.addObserver(object: LifecycleObserver { @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) fun onDestroy() { job.cancel() } }) return job }  
تبسيط الموقف مع الاسترجاعات (الجزء 1)إليك كيفية تحويل استخدام واجهات برمجة التطبيقات المستندة إلى رد الاتصال مع 
Channel .
يعمل API مثل هذا:
- requestBrowsing(url, listener)يوزع المجلد الموجود على- url.
- listenerعلى- onMediaAdded(media: Media)لأي ملف وسائط موجود في هذا المجلد.
- يتم استدعاء listener.onBrowseEnd()عند تحليل المجلد
فيما يلي وظيفة 
refresh القديمة في موفر المحتوى لمتصفح VLC:
 private val refreshList = mutableListOf<Media>() fun refresh() = requestBrowsing(url, refreshListener) private val refreshListener = object : EventListener{ override fun onMediaAdded(media: Media) { refreshList.add(media)) } override fun onBrowseEnd() { val list = refreshList.toMutableList() refreshList.clear() launch { dataset.value = list parseSubDirectories() } } } 
كيفية تحسينه؟قم بإنشاء قناة ستعمل في 
refresh . الآن ستقوم عمليات معاودة الاتصال بالمتصفح بتوجيه الوسائط إلى هذه القناة فقط ، ثم إغلاقها.
الآن أصبحت وظيفة 
refresh أكثر وضوحًا. إنها تنشئ قناة ، وتدعو متصفح VLC ، ثم تشكل قائمة بملفات الوسائط وتعالجها.
بدلاً من 
select أو 
consumeEach يمكنك استخدام انتظار الوسائط ، وسوف تنقطع هذه الحلقة بمجرد إغلاق 
browserChannel .
 private lateinit var browserChannel : Channel<Media> override fun onMediaAdded(media: Media) { browserChannel.offer(media) } override fun onBrowseEnd() { browserChannel.close() } suspend fun refresh() { browserChannel = Channel(Channel.UNLIMITED) val refreshList = mutableListOf<Media>() requestBrowsing(url)  
تبسيط الموقف من خلال عمليات الاسترجاعات (الجزء 2): التحديثيةالطريقة الثانية: نحن لا نستخدم kotlinx coroutines على الإطلاق ، لكننا نستخدم إطار coroutine الأساسي.
ترى كيف تعمل coroutines فعلا!
التفاف وظيفة 
retrofitSuspendCall طلب 
Retrofit Call لجعله وظيفة 
suspend .
باستخدام 
suspendCoroutine ندعو طريقة 
Call.enqueue مؤقتًا عن coroutine. سوف استدعاء رد الاتصال بهذه الطريقة استدعاء 
continuation.resume(response) لاستئناف coroutine مع استجابة من الخادم بمجرد تلقيها.
بعد ذلك ، نحتاج فقط إلى دمج وظائف Retrofit في 
retrofitSuspendCall لإرجاع نتائج الاستعلام باستخدامها.
 suspend inline fun <reified T> retrofitSuspendCall(request: () -> Call <T> ) : Response <T> = suspendCoroutine { continuation -> request.invoke().enqueue(object : Callback<T> { override fun onResponse(call: Call<T>, response: Response<T>) { continuation.resume(response) } override fun onFailure(call: Call<T>, t: Throwable) { continuation.resumeWithException(t) } }) } suspend fun browse(path: String?) = retrofitSuspendCall { ApiClient.browse(path) }  
وبالتالي ، يتم إجراء حظر المكالمات على الشبكة في مؤشر الترابط التحديثي المخصص ، والموقع هنا ، في انتظار استجابة من الخادم ، وليس هناك مكان لاستخدامه في التطبيق!
هذا التطبيق مستوحى من 
مكتبة gildor / kotlin-coroutines-retrofit .
يوجد أيضًا 
محول JakeWharton / retrofit2-kotlin-coroutines مع تطبيق آخر يعطي نتيجة مماثلة.
خاتمةChannel يمكن استخدامها في العديد من الطرق الأخرى. تحقق من 
BroadcastChannel للحصول على تطبيقات أكثر قوة قد تجدها مفيدة.
يمكنك أيضًا إنشاء قنوات باستخدام وظيفة 
Produce .
أخيرًا ، يعد استخدام القنوات مناسبًا لتنظيم الاتصالات بين مكونات واجهة المستخدم: يمكن للمحول نقل أحداث النقر إلى جزءه / نشاطه عبر 
Channel أو ، على سبيل المثال ، من خلال 
Actor .