مرحبا يا هبر!
نذكرك أن لدينا بالفعل طلب
مسبق للكتاب الذي طال انتظاره عن لغة 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
.