RxJava إلى Coroutines: ترحيل ميزة نهاية إلى نهاية

صورة

(نشرت أصلا على متوسطة )

إن corotines Kotlin هي أكثر بكثير من مجرد خيوط خفيفة الوزن - إنها نموذج جديد يساعد المطورين على التعامل مع التزامن بطريقة منظمة وتعبيرية.

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

ميزة


الميزة التي سنقوم بتحويلها إلى coroutines بسيطة إلى حد ما: عندما يرسل المستخدم بلدًا ، نقوم بإجراء مكالمة API للتحقق مما إذا كانت الدولة مؤهلة للبحث عن تفاصيل العمل عبر موفر مثل Companies House . إذا كانت المكالمة ناجحة ، فسنظهر الرد ، إن لم يكن - رسالة الخطأ.

هجرة



سنقوم بترحيل الكود الخاص بنا بطريقة نهائية تبدأ من خدمة التحديثية ، والانتقال إلى طبقة المستودع ، ثم إلى طبقة المتفاعل وأخيراً إلى ViewModel.

الوظائف التي يتم إرجاعها حاليًا يجب أن تصبح الوظيفة "وظائف التعليق" والوظائف التي تُرجع "الملاحظة" يجب أن تُرجع "التدفق". في هذا المثال بالذات ، لن نفعل أي شيء مع التدفقات.

خدمة التحديثية


دعنا ننتقل مباشرة إلى الكود ونعيد قراءة طريقة businessLookupEligibility في BusinessLookupService إلى coroutines. هذه هي الطريقة التي تبدو الآن.

interface BusinessLookupService { @GET("v1/eligibility") fun businessLookupEligibility( @Query("countryCode") countryCode: String ): Single<NetworkResponse<BusinessLookupEligibilityResponse, ErrorResponse>> } 

خطوات إعادة البناء:

  1. بدءاً من الإصدار 2.6.0 يدعم التعديل التحديثي معدل التوقف المرحلي. دعنا نحول طريقة businessLookupEligibility إلى وظيفة تعليق.
  2. قم بإزالة برنامج التفاف واحد من نوع الإرجاع.

 interface BusinessLookupService { @GET("v1/eligibility") suspend fun businessLookupEligibility( @Query("countryCode") countryCode: String ): NetworkResponse<BusinessLookupEligibilityResponse, ErrorResponse> } 

NetworkResponse هي فئة مختومة تمثل BusinessLookupEligibilityResponse أو ErrorResponse. يتم إنشاء NetworkResponse في محول استدعاء التحديثية مخصص. وبهذه الطريقة ، نقصر تدفق البيانات على حالتين محتملتين فقط - النجاح أو الخطأ ، لذلك لا داعي لقلق مستهلكي BusinessLookupService بشأن معالجة الاستثناء.

مستودع


دعنا ننتقل ونرى ما لدينا في BusinessLookupRepository. في هيئة طريقة businessLookupEligibility ، ندعو businessLookupService.businessLookupEligibility (تلك التي قمنا بإعادة تشكيلها للتو) ونستخدم مشغل خرائط RxJava لتحويل NetworkResponse إلى نموذج استجابة للنتائج والخرائط لنموذج المجال. النتيجة هي فئة مختومة أخرى تمثل Result.Success وتحتوي على كائن BusinessBooklEligibility في حالة نجاح اتصال الشبكة. إذا حدث خطأ في استدعاء الشبكة أو استثناء إلغاء التسلسل أو حدث خطأ آخر ، فسننشئ Result.Failure برسالة خطأ ذات معنى (ErrorMessage هو typealias for String).

 class BusinessLookupRepository @Inject constructor( private val businessLookupService: BusinessLookupService, private val businessLookupApiToDomainMapper: BusinessLookupApiToDomainMapper, private val responseToString: Mapper, private val schedulerProvider: SchedulerProvider ) { fun businessLookupEligibility(countryCode: String): Single<Result<BusinessLookupEligibility, ErrorMessage>> { return businessLookupService.businessLookupEligibility(countryCode) .map { response -> return@map when (response) { is NetworkResponse.Success -> { val businessLookupEligibility = businessLookupApiToDomainMapper.map(response.body) Result.Success<BusinessLookupEligibility, ErrorMessage>(businessLookupEligibility) } is NetworkResponse.Error -> Result.Failure<BusinessLookupEligibility, ErrorMessage>( responseToString.transform(response) ) } }.subscribeOn(schedulerProvider.io()) } } 

خطوات إعادة البناء:

  1. businessLookupEligibility تصبح وظيفة تعليق مؤقت.
  2. قم بإزالة برنامج التفاف واحد من نوع الإرجاع.
  3. عادةً ما تقوم الطرق الموجودة في المستودع بمهام طويلة الأمد مثل مكالمات الشبكة أو استعلامات الديسيبل. يقع على عاتق المستودع مسؤولية تحديد السلسلة التي يجب أن يتم هذا العمل عليها. من خلال subscribeOn (schedulerProvider.io ()) ، فإننا نبلغ RxJava بأنه يجب القيام بالعمل على مؤشر ترابط io. كيف يمكن أن يتحقق الشيء نفسه مع coroutines؟ سنستخدم withContext مع مرسل معين لتحويل تنفيذ الكتلة إلى مؤشر ترابط مختلف والعودة إلى المرسل الأصلي عند اكتمال التنفيذ. إنها ممارسة جيدة للتأكد من أن الوظيفة آمنة من خلال استخدام withContext. يجب على مستهلكي BusinessLookupRepository ألا يفكروا في سلسلة الرسائل التي يجب عليهم استخدامها لتنفيذ طريقة businessLookupEligibility ، يجب أن يكون من الآمن استدعاءها من سلسلة الرسائل الرئيسية.
  4. لم نعد نحتاج إلى مشغل الخريطة ، حيث يمكننا استخدام نتيجة businessLookupService.businessLookupElibility في جسم وظيفة التعليق.

 class BusinessLookupRepository @Inject constructor( private val businessLookupService: BusinessLookupService, private val businessLookupApiToDomainMapper: BusinessLookupApiToDomainMapper, private val responseToString: Mapper, private val dispatcherProvider: DispatcherProvider ) { suspend fun businessLookupEligibility(countryCode: String): Result<BusinessLookupEligibility, ErrorMessage> = withContext(dispatcherProvider.io) { when (val response = businessLookupService.businessLookupEligibility(countryCode)) { is NetworkResponse.Success -> { val businessLookupEligibility = businessLookupApiToDomainMapper.map(response.body) Result.Success<BusinessLookupEligibility, ErrorMessage>(businessLookupEligibility) } is NetworkResponse.Error -> Result.Failure<BusinessLookupEligibility, ErrorMessage>( responseToString.transform(response) ) } } } 

interactor ل


في هذا المثال بالتحديد ، لا يحتوي BusinessLookupEligibilityInteractor على أي منطق إضافي ويعمل كوكيل لـ BusinessLookupRepository. نحن نستخدم استدعاء عامل التحميل الزائد بحيث يمكن استدعاء المتفاعل كدالة.

 class BusinessLookupEligibilityInteractor @Inject constructor( private val businessLookupRepository: BusinessLookupRepository ) { operator fun invoke(countryCode: String): Single<Result<BusinessLookupEligibility, ErrorMessage>> = businessLookupRepository.businessLookupEligibility(countryCode) } 

خطوات إعادة البناء:

  1. استدعاء المشغل المرح يصبح تعليق استدعاء المشغل المرح.
  2. قم بإزالة برنامج التفاف واحد من نوع الإرجاع.

 class BusinessLookupEligibilityInteractor @Inject constructor( private val businessLookupRepository: BusinessLookupRepository ) { suspend operator fun invoke(countryCode: String): Result<BusinessLookupEligibility, ErrorMessage> = businessLookupRepository.businessLookupEligibility(countryCode) } 

ViewModel


في BusinessProfileViewModel نسمي BusinessLookupEligibilityInteractor بإرجاع Single. نحن نشترك في الدفق ونلاحظه في مؤشر ترابط واجهة المستخدم بتحديد جدولة واجهة المستخدم. في حالة النجاح ، نقوم بتعيين القيمة من نموذج مجال إلى BusinessViewState LiveData. في حالة الفشل ، نقوم بتعيين رسالة خطأ.

نضيف كل اشتراك إلى CompositeDisposable ونتخلص منهم في طريقة onCleared () لدورة حياة ViewModel.

 class BusinessProfileViewModel @Inject constructor( private val businessLookupEligibilityInteractor: BusinessLookupEligibilityInteractor, private val schedulerProvider: SchedulerProvider ) : ViewModel() { private val disposables = CompositeDisposable() internal val businessViewState: MutableLiveData<ViewState> = LiveDataFactory.createDefault("Loading...") fun onCountrySubmit(country: Country) { disposables.add(businessLookupEligibilityInteractor(country.countryCode) .observeOn(schedulerProvider.ui()) .subscribe { state -> return@subscribe when (state) { is Result.Success -> businessViewState.value = state.entity.provider is Result.Failure -> businessViewState.value = state.failure } }) } @Override protected void onCleared() { super.onCleared(); disposables.clear(); } } 

خطوات إعادة البناء:

  1. في بداية المقال ، ذكرت واحدة من المزايا الرئيسية لل coroutines - التزامن المنظم. وهنا يأتي دور اللعبة. كل coroutine لديه نطاق. النطاق لديه السيطرة على coroutine عبر وظيفتها. إذا تم إلغاء الوظيفة ، فسيتم أيضًا إلغاء جميع الخطوط في النطاق المقابل. أنت حر في إنشاء نطاقات خاصة بك ، لكن في هذه الحالة ، سنستفيد من viewModelScope المدرك لدورة حياة دورة الحياة. سنبدأ coroutine جديد في viewModelScope باستخدام viewModelScope.launch. سيتم إطلاق coroutine في الخيط الرئيسي لأن viewModelScope لديه مرسل افتراضي - Dispatchers.Main. بدأ coroutine على Dispatchers. لن يقوم Main بحظر الخيط الرئيسي أثناء تعليقه. نظرًا لأننا أطلقنا للتو coroutine ، يمكننا استدعاء businessLookupEligibilityInteractor بتعليق المشغل والحصول على النتيجة. businessLookupEligibilityInteractor يستدعي BusinessLookupRepository.businessLookupEligibility ما يحول التنفيذ إلى Dispatchers.IO ويعود إلى Dispatchers.Main. بما أننا في مؤشر ترابط واجهة المستخدم ، يمكننا تحديث businessViewState LiveData عن طريق تعيين قيمة.
  2. يمكننا التخلص من المستهلكات لأن viewModelScope مرتبط بدورة حياة ViewModel. يتم إلغاء أي coroutine الذي تم إطلاقه في هذا النطاق تلقائيًا إذا تم مسح ViewModel.

 class BusinessProfileViewModel @Inject constructor( private val businessLookupEligibilityInteractor: BusinessLookupEligibilityInteractor ) : ViewModel() { internal val businessViewState: MutableLiveData<ViewState> = LiveDataFactory.createDefault("Loading...") fun onCountrySubmit(country: Country) { viewModelScope.launch { when (val state = businessLookupEligibilityInteractor(country.countryCode)) { is Result.Success -> businessViewState.value = state.entity.provider is Result.Failure -> businessViewState.value = state.failure } } } } 

الوجبات السريعة الرئيسية


إن قراءة وفهم الكود المكتوب مع coroutines سهل للغاية ، ومع ذلك فهو تحول نموذجي يتطلب بعض الجهد لتعلم كيفية التعامل مع كتابة التعليمات البرمجية مع coroutines.

في هذه المقالة لم أغطي الاختبار. لقد استخدمت مكتبة mockk حيث واجهت مشاكل في اختبار coroutines باستخدام Mockito.

كل ما كتبته باستخدام Rx Java ، وجدته سهل التنفيذ مع coroutines والتدفقات والقنوات . واحدة من مزايا coroutines هو أنها ميزة لغة Kotlin وتتطور جنبا إلى جنب مع اللغة.

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


All Articles