مرحبا يا هبر! أقدم لكم ترجمة
لمقالة بقلم باولو ساتو حول استخدام Kotlin Coroutines بدلاً من RxJava في مشاريع Android الخاصة بهم.
RxJava كبازوكا ، معظم التطبيقات لا تستخدم حتى نصف قوتها النارية. ستناقش المقالة كيفية استبداله بـ Corotines (Kotlin).
أعمل مع RxJava منذ عدة سنوات. هذه بالتأكيد واحدة من أفضل المكتبات لأي مشروع Android ، والتي لا تزال في حالة صدمة اليوم ، خاصة إذا كنت تقوم بالبرمجة في Java. إذا كنت تستخدم Kotlin ، فيمكننا القول أن المدينة لديها شريف جديد.
يستخدم معظم RxJava فقط للتحكم في سلاسل الرسائل ومنع رد الاتصال (إذا كنت لا تعرف ماهيتها ، اعتبر نفسك محظوظًا
ولهذا السبب ). والحقيقة هي أنه يجب أن نضع في اعتبارنا أن القوة الحقيقية لـ RxJava هي البرمجة التفاعلية والضغط الخلفي. إذا كنت تستخدمه للتحكم في الطلبات غير المتزامنة ، فإنك تستخدم البازوكا لقتل العنكبوت. ستقوم بعملها ، لكنها مبالغة.
أحد العوائق البارزة لـ RxJava هو عدد الطرق. إنه ضخم ويميل إلى الانتشار في جميع أنحاء الكود. في Kotlin ، يمكنك استخدام coroutines لتنفيذ معظم السلوك الذي قمت بإنشائه سابقًا باستخدام RxJava.
ولكن ... ما هي coroutines؟
Corutin هو طريقة للتعامل مع المهام التنافسية في خيط. ستعمل سلسلة المحادثات حتى يتم إيقافها وسيتغير السياق لكل coroutine دون إنشاء مؤشر ترابط جديد.
لا تزال coroutines في Kotlin تجريبية ، ولكن تم تضمينها في Kotlin 1.3 ، لذلك كتبت فئة UseCase جديدة (للبناء النظيف) باستخدامها أدناه. في هذا المثال ، يتم تغليف مكالمة coroutine في ملف واحد. وبالتالي ، فإن الطبقات الأخرى لن تعتمد على الكورونات التي يتم تنفيذها ، مما يوفر بنية أكثر انقطاعًا.
package com.psato.devcamp.interactor.usecase import android.util.Log import kotlinx.coroutines.experimental.* import kotlinx.coroutines.experimental.android.UI import kotlin.coroutines.experimental.CoroutineContext abstract class UseCase<T> { protected var parentJob: Job = Job() //var backgroundContext: CoroutineContext = IO var backgroundContext: CoroutineContext = CommonPool var foregroundContext: CoroutineContext = UI protected abstract suspend fun executeOnBackground(): T fun execute(onComplete: (T) -> Unit, onError: (Throwable) -> Unit) { parentJob.cancel() parentJob = Job() launch(foregroundContext, parent = parentJob) { try { val result = withContext(backgroundContext) { executeOnBackground() } onComplete.invoke(result) } catch (e: CancellationException) { Log.d("UseCase", "canceled by user") } catch (e: Exception) { onError(e) } } } protected suspend fun <X> background(context: CoroutineContext = backgroundContext, block: suspend () -> X): Deferred<X> { return async(context, parent = parentJob) { block.invoke() } } fun unsubscribe() { parentJob.cancel() } }
بادئ ذي بدء ، أنشأت مهمة رئيسية. هذا هو مفتاح التراجع عن جميع coroutines التي تم إنشاؤها في فئة UseCase. عندما نتصل بالتنفيذ ، من المهم إلغاء المهام القديمة ، للتأكد من أننا لم نفتقد أي كوروتين (هذا سيحدث أيضًا إذا قمنا بإلغاء الاشتراك في UseCase).
أيضا ، أنا استدعاء بدء التشغيل (UI). هذا يعني أنني أريد إنشاء coroutine التي سيتم تنفيذها في مؤشر ترابط واجهة المستخدم. بعد ذلك ، أسمي طريقة الخلفية التي تخلق عدم التزامن في CommonPool (سيكون هذا الأسلوب في الواقع ضعيف الأداء). بدوره ، سيعيد async Deffered ، وبعد ذلك ، سأدعو طريقة الانتظار الخاصة به. وينتظر استكمال الخلفية الخلفية ، والتي ستؤدي إلى نتيجة أو خطأ.
يمكن استخدام هذا لتنفيذ معظم كل ما قمنا به مع RxJava. فيما يلي بعض الأمثلة.
الخريطة
لقد قمت بتنزيل نتائج searchShow وقمت بتغييرها لإرجاع اسم العرض الأول.
كود RxJava:
public class SearchShows extends UseCase { private ShowRepository showRepository; private ResourceRepository resourceRepository; private String query; @Inject public SearchShows(ShowRepository showRepository, ResourceRepository resourceRepository) { this.showRepository = showRepository; this.resourceRepository = resourceRepository; } public void setQuery(String query) { this.query = query; } @Override protected Single<String> buildUseCaseObservable() { return showRepository.searchShow(query).map(showInfos -> { if (showInfos != null && !showInfos.isEmpty() && showInfos.get(0).getShow() != null) { return showInfos.get(0).getShow().getTitle(); } else { return resourceRepository.getNotFoundShow(); } }); } }
كود Coroutine:
class SearchShows @Inject constructor(private val showRepository: ShowRepository, private val resourceRepository: ResourceRepository) : UseCase<String>() { var query: String? = null override suspend fun executeOnBackground(): String { query?.let { val showsInfo = showRepository.searchShow(it) val showName: String? = showsInfo?.getOrNull(0)?.show?.title return showName ?: resourceRepository.notFoundShow } return "" } }
الرمز البريدي
سيأخذ Zip إنبعاث من الأوبزرفر ويجمعهما في انبعاث جديد. لاحظ أنه مع RxJava يجب عليك تحديد إجراء مكالمة بالتوازي باستخدام SubscribeOn في كل واحدة. نريد الحصول على كليهما في نفس الوقت وإعادتهما معًا.
كود RxJava:
public class ShowDetail extends UseCase { private ShowRepository showRepository; private String id; @Inject public SearchShows(ShowRepository showRepository) { this.showRepository = showRepository; } public void setId(String id) { this.id = id; } @Override protected Single<Show> buildUseCaseObservable() { Single<ShowDetail> singleDetail = showRepository.showDetail(id).subscribeOn(Schedulers.io()); Single<ShowBanner> singleBanner = showRepository.showBanner(id).subscribeOn(Schedulers.io()); return Single.zip(singleDetail, singleBanner, (detail, banner) -> new Show(detail,banner)); }
كود Coroutine:
class SearchShows @Inject constructor(private val showRepository: ShowRepository, private val resourceRepository: ResourceRepository) : UseCase<Show>() { var id: String? = null override suspend fun executeOnBackground(): Show { id?.let { val showDetail = background{ showRepository.showDetail(it) } val showBanner = background{ showRepository.showBanner(it) } return Show(showDetail.await(), showBanner.await()) } return Show() } }
خريطة مسطحة
في هذه الحالة ، أبحث عن العروض التي تحتوي على سلسلة استعلام ولكل نتيجة (تقتصر على 200 نتيجة) ، أحصل أيضًا على تصنيف العرض. في النهاية ، أعود قائمة بالعروض ذات التقييمات المقابلة.
كود RxJava:
public class SearchShows extends UseCase { private ShowRepository showRepository; private String query; @Inject public SearchShows(ShowRepository showRepository) { this.showRepository = showRepository; } public void setQuery(String query) { this.query = query; } @Override protected Single<List<ShowResponse>> buildUseCaseObservable() { return showRepository.searchShow(query).flatMapPublisher( (Function<List<ShowInfo>, Flowable<ShowInfo>>) Flowable::fromIterable) .flatMapSingle((Function<ShowInfo, SingleSource<ShowResponse>>) showInfo -> showRepository.showRating(showInfo.getShow().getIds().getTrakt()) .map(rating -> new ShowResponse(showInfo.getShow().getTitle(), rating .getRating())).subscribeOn(Schedulers.io()), false, 4).toList(); } }
كود Coroutine:
class SearchShows @Inject constructor(private val showRepository: ShowRepository) : UseCase<List<ShowResponse>>() { var query: String? = null override suspend fun executeOnBackground(): List<ShowResponse> { query?.let { query -> return showRepository.searchShow(query).map { background { val rating: Rating = showRepository.showRating(it.show!!.ids!!.trakt!!) ShowResponse(it.show.title!!, rating.rating) } }.map { it.await() } } return arrayListOf() } }
دعني أشرح. باستخدام RxJava ، يقوم المستودع الخاص بي بإرجاع إصدار واحد من القائمة ، لذلك أحتاج إلى العديد من الانبعاثات ، واحد لكل ShowInfo. للقيام بذلك ، دعوت flatMapPublisher. لكل مشكلة ، يجب أن أسلط الضوء على ShowResponse ، وفي النهاية أجمعهم جميعًا في قائمة.
ينتهي بنا المطاف بهذا البناء: List foreach → (ShowInfo → ShowRating → ShowResponse) → List.
باستخدام coroutines ، قمت بعمل خريطة لكل عنصر قائمة لتحويله إلى قائمة <Deffered>.
كما ترون ، فإن معظم ما قمنا به مع RxJava أسهل في التنفيذ مع المكالمات المتزامنة. يمكن لـ Coroutines حتى التعامل مع FlatMap ، والتي أعتقد أنها واحدة من أكثر الوظائف تعقيدًا في RxJava.
من المعروف جيدًا أن coroutines يمكن أن تكون خفيفة الوزن (
هنا مثال) ، لكن النتائج حيرتني. في هذا المثال ، بدأ RxJava في حوالي 3.1 ثانية ، في حين استغرق coroutines حوالي 5.8 ثانية للتشغيل على CommonPool.
أثارت هذه النتائج السؤال المطروح قبلي بأنه قد يكون هناك شيء غير مناسب فيها. في وقت لاحق ، وجدت هذا. لقد استخدمت التحديث التحديثي ، الذي منع التدفق.
هناك طريقتان لإصلاح هذا الخطأ ، يعتمد الاختيار على إصدار Android Studio الذي تستخدمه. في Android Studio 3.1 ، نحتاج إلى التأكد من عدم حظر سلسلة المحادثات في الخلفية. لهذا ، استخدمت هذه المكتبة:
تنفيذ "ru.gildor.coroutines: kotlin-coroutines-التحديثية: 0.12.0"
ينشئ هذا الرمز امتدادًا لوظيفة استدعاء التحديث لإيقاف الدفق مؤقتًا:
public suspend fun <T : Any> Call<T>.await(): T { return suspendCancellableCoroutine { continuation -> enqueue(object : Callback<T> { override fun onResponse(call: Call<T>?, response: Response<T?>) { if (response.isSuccessful) { val body = response.body() if (body == null) { continuation.resumeWithException( NullPointerException("Response body is null: $response") ) } else { continuation.resume(body) } } else { continuation.resumeWithException(HttpException(response)) } } override fun onFailure(call: Call<T>, t: Throwable) {
في Android Studio 3.2 ، يمكنك تحديث مكتبة corutin إلى الإصدار 0.25.0. يحتوي هذا الإصدار على CoroutineContext IO (يمكنك مشاهدة التعليق المقابل في فئة UseCase الخاصة بي).
استغرق العمل على CommonPool دون مكالمة حظر تستغرق 2.3 ثانية و 2.4 ثانية مع IO وحظر المكالمات.

آمل أن تلهمك هذه المقالة لاستخدام corutin ، وهو بديل أخف وربما أسرع لـ RxJava ويجعل الأمر أسهل قليلاً لفهم أنك تقوم بكتابة كود متزامن يعمل بشكل غير متزامن.