
مرحبا بالجميع! اسمي Anatoly Varivonchik ، أنا مطور Android في Badoo. سأشارككم اليوم ترجمة الجزء الثاني من المقالة من قبل زميلي زولت كوتشسي حول تنفيذ MVI ، والتي نستخدمها يوميًا في عملية التطوير. الجزء الأول
هنا .
ما نريد وكيف نفعل ذلك
في الجزء الأول من المقالة ، قدمنا
الميزات ، وهي العناصر الرئيسية لـ
MVICore التي يمكن إعادة استخدامها. يمكن أن يكون لديهم أبسط بنية
ويتضمن مخفضًا واحدًا فقط ، أو يمكنهم أن يصبحوا أداة فعالة بالكامل لإدارة المهام والأحداث غير المتزامنة وغير ذلك الكثير.
كل ميزة يمكن تتبعها - هناك فرصة للاشتراك في التغييرات في حالتها وتلقي الإخطارات حولها. في هذه الحالة ، يمكن الاشتراك في الميزة في مصدر الإدخال. وهذا منطقي ، لأنه مع تضمين Rx في قاعدة الكود ، لدينا بالفعل الكثير من الأشياء والاشتراكات التي يمكن ملاحظتها على مستويات مختلفة.
فيما يتعلق بالزيادة في عدد العناصر التفاعلية ، فقد حان الوقت للتفكير في ما لدينا وما إذا كان من الممكن تحسين النظام.
يجب أن نجيب على ثلاثة أسئلة:
- ما العناصر التي يجب استخدامها عند إضافة مكونات تفاعلية جديدة؟
- ما هي أسهل طريقة لإدارة اشتراكاتك؟
- هل من الممكن تجاهل إدارة دورة الحياة / الحاجة إلى مسح الاشتراكات لتجنب تسرب الذاكرة؟ بمعنى آخر ، هل يمكننا فصل ربط المكونات عن إدارة الاشتراك؟
في هذا الجزء من المقالة ، سنلقي نظرة على أساسيات وفوائد بناء نظام باستخدام مكونات تفاعلية ونرى كيف يساعد Kotlin في ذلك.
العناصر الرئيسية
في الوقت الذي بدأنا فيه العمل على تصميم وتوحيد
ميزاتنا ، جربنا بالفعل العديد من الأساليب المختلفة وقررنا أن
الميزات ستكون في شكل مكونات تفاعلية. أولاً ، ركزنا على الواجهات الرئيسية. بادئ ذي بدء ، كنا بحاجة إلى تحديد أنواع بيانات المدخلات والمخرجات.
استدركنا ما يلي:
- دعونا لا نعيد ابتكار العجلة - دعنا نرى ما هي الواجهات الموجودة بالفعل.
- نظرًا لأننا نستخدم بالفعل مكتبة RxJava ، فمن المنطقي الرجوع إلى واجهاتها الأساسية.
- يجب تقليل عدد الواجهات.
ونتيجة لذلك ، قررنا استخدام
ObservableSource <T> للإخراج و
Consumer <T> للإدخال. تسأل لماذا لا
يمكن ملاحظته / مراقب .
Observable عبارة عن فئة مجردة تحتاج إلى الوراثة منها ، و
ObservableSource هي الواجهة التي تنفذها والتي تلبي بشكل كامل الحاجة إلى تنفيذ بروتوكول تفاعلي.
package io.reactivex; import io.reactivex.annotations.*; public interface ObservableSource<T> { void subscribe(@NonNull Observer<? super T> observer); }
يطبق برنامج
Observer ، أول واجهة تتبادر إلى الذهن ، أربع طرق: onSubscribe و onNext و onError و onComplete. في محاولة لتبسيط البروتوكول قدر الإمكان ، فضلنا
المستهلك <T> ، الذي يقبل العناصر الجديدة باستخدام طريقة واحدة. إذا اخترنا
Observer ، فستكون الطرق المتبقية غالبًا ما تكون زائدة عن الحاجة أو تعمل بشكل مختلف (على سبيل المثال ، نود تقديم الأخطاء كجزء من الحالة ، وليس كاستثناءات ، وبالتأكيد لا تقاطع الدفق).
public interface Consumer<T> { void accept(T t) throws Exception; }
لذلك ، لدينا واجهتان ، يحتوي كل منهما على طريقة واحدة. الآن يمكننا ربطهم من خلال توقيع
المستهلك <T> على
ObservableSource <T> . يقبل هذا الأخير فقط أمثلة
المراقب <T> ، ولكن يمكننا
لفه في
ملاحظة يمكن
ملاحظتها <T> ، والتي تشترك في
المستهلك <T> :
val output: ObservableSource<String> = Observable.just("item1", "item2", "item3") val input: Consumer<String> = Consumer { System.out.println(it) } val disposable = Observable.wrap(output).subscribe(input)
(لحسن الحظ ، لا تقوم وظيفة
.wrap (الإخراج) بإنشاء كائن جديد إذا كان
الإخراج بالفعل
<T> يمكن ملاحظته ).
قد تتذكر أن مكوِّن
الميزة من الجزء الأول من المقالة استخدم بيانات الإدخال من النوع
Wish (المقابل لـ Intent من Model-View-Intent) ومخرجات الحالة من النوع ، وبالتالي يمكن أن يكون على جانبي الحزمة:
يبدو هذا الربط بين
المستهلك والمنتج بسيطًا بالفعل ، ولكن هناك طريقة أسهل لا تحتاج فيها إلى إنشاء اشتراكات يدويًا أو إلغائها.
إدخال
بيندر .
ربط الستيرويد
يحتوي
MVICore على فئة تسمى
Binder التي توفر واجهة برمجة تطبيقات بسيطة لإدارة اشتراكات Rx ولديها عدد من الميزات الرائعة.
لماذا هو مطلوب؟
- قم بإنشاء ربط عن طريق الاشتراك في عطلة نهاية الأسبوع.
- القدرة على إلغاء الاشتراك في نهاية دورة الحياة (عندما يكون مفهومًا مجردًا ولا علاقة له بنظام Android).
- المكافأة: يسمح لك Binder بإضافة كائنات وسيطة ، على سبيل المثال ، لتسجيل الدخول أو تصحيح أخطاء السفر عبر الزمن.
بدلاً من التوقيع يدويًا ، يمكنك إعادة كتابة الأمثلة أعلاه على النحو التالي:
val binder = Binder() binder.bind(wishes to feature) binder.bind(feature to logger)
بفضل Kotlin ، يبدو كل شيء بسيطًا جدًا.
تعمل هذه الأمثلة إذا كان نوع الإدخال والإخراج هو نفسه. ولكن ماذا لو لم يكن كذلك؟ من خلال تنفيذ وظيفة التمديد ، يمكننا جعل التحويل تلقائيًا:
val output: ObservableSource<A> = TODO() val input: Consumer<B> = TODO() val transformer: (A) -> B = TODO() binder.bind(output to input using transformer)
انتبه إلى البنية: فهي تقرأ تقريبًا مثل جملة عادية (وهذا سبب آخر لأني أحب Kotlin). لكن
Binder لا يستخدم فقط كسكر نحوي - بل إنه مفيد لنا أيضًا لحل المشكلات في دورة الحياة.
إنشاء بيندر
إنشاء مثيل يبدو أسهل في أي مكان:
val binder = Binder()
ولكن في هذه الحالة ، تحتاج إلى إلغاء الاشتراك يدويًا ، ويجب عليك استدعاء
binder.dispose()
كلما كان من الضروري حذف الاشتراكات. هناك طريقة أخرى: حقن مثيل دورة الحياة في المنشئ. مثل هذا:
val binder = Binder(lifecycle)
الآن لا داعي للقلق بشأن الاشتراكات - سيتم حذفها في نهاية دورة الحياة. في الوقت نفسه ، يمكن تكرار دورة الحياة عدة مرات (مثل دورة البدء والإيقاف في واجهة مستخدم Android) - وسيقوم
Binder بإنشاء الاشتراكات وحذفها لك في كل مرة.
وما هي دورة الحياة؟
معظم مطوري Android ، الذين يرون عبارة "دورة الحياة" ، يمثلون دورات النشاط والجزء. نعم ، يمكن لـ
Binder العمل معهم ، إلغاء الاشتراك في نهاية الدورة.
ولكن هذه ليست سوى البداية ، لأنك لا تستخدم واجهة Android
LifecycleOwner بأي شكل من الأشكال - لدى
Binder واحدة خاصة بها وأكثر عالمية. إنه في الأساس دفق إشارة BEGIN / END:
interface Lifecycle : ObservableSource<Lifecycle.Event> { enum class Event { BEGIN, END }
يمكنك إما تنفيذ هذا الدفق باستخدام الملاحظة (عن طريق التعيين) ، أو ببساطة استخدام فئة
ManualLifecycle من المكتبة للبيئات غير Rx (انظر بالضبط أدناه).
كيف يعمل
الموثق ؟ باستقبال إشارة BEGIN ، فإنه ينشئ اشتراكات للمكونات التي قمت بتكوينها مسبقًا (
الإدخال / الإخراج ) ، وتلقي إشارة END ، وحذفها. الشيء الأكثر إثارة للاهتمام هو أنه يمكنك البدء من جديد:
val output: PublishSubject<String> = PublishSubject.create() val input: Consumer<String> = Consumer { System.out.println(it) } val lifecycle = ManualLifecycle() val binder = Binder(lifecycle) binder.bind(output to input) output.onNext("1") lifecycle.begin() output.onNext("2") output.onNext("3") lifecycle.end() output.onNext("4") lifecycle.begin() output.onNext("5") output.onNext("6") lifecycle.end() output.onNext("7")
هذه المرونة في إعادة تعيين الاشتراكات مفيدة بشكل خاص عند العمل مع Android ، عندما يمكن أن يكون هناك العديد من دورات Start-Stop و Resume-Pause ، بالإضافة إلى Create-Destroy المعتاد.
دورات حياة Android Binder
هناك ثلاثة فصول في المكتبة:
- CreateDestroyBinderLifecycle ( دورة حياة android )
- StartStopBinderLifecycle ( دورة حياة android )
- ResumePauseBinderLifecycl e ( androidLifecycle )
androidLifecycle
هي القيمة التي يتم إرجاعها بواسطة طريقة
getLifecycle()
، وهي
AppCompatActivity و
AppCompatDialogFragment وما إلى ذلك. كل شيء بسيط للغاية:
fun createBinderForActivity(activity: AppCompatActivity) = Binder( CreateDestroyBinderLifecycle(activity.lifecycle) )
دورات الحياة الفردية
دعونا لا نتوقف عند هذا الحد ، لأننا لسنا مرتبطين بأندرويد بأي شكل من الأشكال. ما هي دورة حياة
الموثق ؟ حرفياً أي شيء: على سبيل المثال ، وقت تشغيل مربع حوار أو وقت تنفيذ بعض المهام غير المتزامنة. يمكنك ، على سبيل المثال ، ربطه بنطاق DI - وبعد ذلك سيتم حذف أي اشتراك معه. الحرية الكاملة للعمل.
- هل تريد حفظ الاشتراكات قبل أن يرصد Observable العنصر؟ تحويل هذا الكائن إلى دورة الحياة وتمريره إلى Binder . قم بتنفيذ الكود التالي في وظيفة الامتداد واستخدمه لاحقًا:
fun Observable<T>.toBinderLifecycle() = Lifecycle.wrap(this .first() .map { END } .startWith(BEGIN) )
- هل تريد الحفاظ على ارتباطاتك حتى انتهاء Completable ؟ لا توجد مشاكل - يتم ذلك عن طريق القياس مع الفقرة السابقة:
fun Completable.toBinderLifecycle() = Lifecycle.wrap( Observable.concat( Observable.just(BEGIN), this.andThen(Observable.just(END)) ) )
- هل تريد بعض التعليمات البرمجية الأخرى غير Rx لتحديد موعد إزالة الاشتراكات؟ استخدم ManualLifecycle كما هو موضح أعلاه.
في أي حال ، يمكنك إما وضع الدفق التفاعلي
لدورة حياة Lifecycle.Event ، أو استخدام
ManualLifecycle إذا كنت تعمل برمز غير Rx.
نظرة عامة على النظام
يخفي
Binder تفاصيل إنشاء اشتراكات Rx وإدارتها. كل ما تبقى هو نظرة عامة موجزة: "يتفاعل المكون أ مع المكون ب في النطاق ج".
لنفترض أن لدينا المكونات التفاعلية التالية للشاشة الحالية:

نرغب في توصيل المكونات داخل الشاشة الحالية ، ونعلم أن:
- يمكن تغذية UIEvent مباشرة إلى AnalyticsTracker ؛
- يمكن تحويل UIEvent إلى رغبة في الميزة ؛
- يمكن تحويل الدولة إلى ViewModel لطريقة عرض .
يمكن التعبير عن ذلك في سطرين:
with(binder) { bind(feature to view using stateToViewModelTransformer) bind(view to feature using uiEventToWishTransformer) bind(view to analyticsTracker) }
نقوم بضغط من هذا القبيل لإظهار الترابط بين المكونات. وبما أن المطورين يقضون وقتًا في قراءة التعليمات البرمجية أكثر من كتابة التعليمات البرمجية ، فإن هذه النظرة العامة الموجزة مفيدة للغاية ، خاصةً مع زيادة عدد المكونات.
الخلاصة
لقد رأينا كيف يساعد
Binder في إدارة اشتراكات Rx وكيف يساعدك في الحصول على نظرة عامة على نظام مبني من مكونات تفاعلية.
في المقالات التالية ، سنخبرنا كيف نفصل بين مكونات واجهة المستخدم التفاعلية من منطق الأعمال وكيفية إضافة كائنات وسيطة باستخدام
Binder (لتسجيل الدخول وتصحيح أخطاء السفر عبر الزمن). لا تقم بالتبديل!
في هذه الأثناء ، تحقق من المكتبة على
GitHub .