العمارة EBA ويعرف أيضا باسم التفاعل الكامل

لقد جئت إلى Tinkoff قبل عامين ، في مشروع جديد ، العملاء والمشاريع ، الذي بدأ للتو في ذلك الوقت.
الآن لا أتذكر مشاعري من العمارة الجديدة آنذاك. لكنني أتذكر بالتأكيد: كان من غير المعتاد استخدام Rx في مكان آخر ، خارج الرحلات المعتادة إلى الشبكة وإلى القاعدة. الآن بعد أن مرت هذه البنية بالفعل ببعض مسار التطور التطوري ، أريد أن أتحدث أخيرًا عن ما حدث وما الذي حدث.



في رأيي ، جميع البنى الشائعة حاليًا - MVP و MVVM وحتى MVI - كانت منذ فترة طويلة في الحلبة ولم تكن تستحقها دائمًا. أليس لديهم عيوب؟ أرى الكثير منهم. قررنا في مكاننا أنه يكفي لتحمله ، و (إعادة) اخترع بنية جديدة غير متزامنة.


سوف أصف بإيجاز ما لا يعجبني في المباني الحالية. بعض النقاط قد تكون مثيرة للجدل. ربما لم تصادف هذا مطلقًا ، فأنت تكتب برمجة جيدي بشكل عام. سامحني يا آثم.
لذلك الألم هو:


  • مقدم ضخم / ViewModel.
  • كمية كبيرة من حالة التبديل في MVI.
  • عدم القدرة على إعادة استخدام أجزاء من مقدم / ViewModel ، ونتيجة لذلك ، الحاجة إلى تكرار التعليمات البرمجية.
  • أكوام من المتغيرات القابلة للتغيير التي يمكن تعديلها من أي مكان. وفقًا لذلك ، يصعب الحفاظ على هذا الرمز وتعديله.
  • لا تتحلل تحديث الشاشة.
  • من الصعب أن تكتب الاختبارات.

مشاكل


في كل لحظة من الزمن ، يكون للتطبيق حالة معينة تحدد سلوكه وما يراه المستخدم. تتضمن هذه الحالة جميع قيم المتغيرات - من الإشارات البسيطة إلى الكائنات الفردية. يعيش كل واحد من هذه المتغيرات حياته الخاصة ويتم التحكم فيه بواسطة أجزاء مختلفة من التعليمات البرمجية. يمكنك تحديد الحالة الحالية للتطبيق فقط عن طريق التحقق منها كلها ، واحدة تلو الأخرى.
مقال عن هندسة Kotlin MVI الحديثة


الفصل 1. التطور هو كل شيء لدينا


في البداية ، كتبنا على MVP ، ولكن تحور قليلا. كان مزيج من MVP و MVI. كانت هناك كيانات من MVP في شكل مقدم وواجهة عرض:


interface NewTaskView { val newTaskAction: Observable<NewTaskAction> val taskNameChangeAction: Observable<String> val onChangeState: Consumer<SomeViewState> } 

بالفعل ، يمكنك ملاحظة المصيد: العرض هنا بعيد جدًا عن شرائع MVP. كانت هناك طريقة في مقدم العرض:


 fun bind(view: SomeView): Disposable 

في الخارج ، تم تمرير تنفيذ واجهة اشتركت تفاعلي في تغييرات واجهة المستخدم. وانها بالفعل صفعات من MVI!


أكثر هو أكثر. في مقدم العرض ، تم إنشاء وتفاعل مشتركين مختلفين في "عرض التغييرات" ، لكنهم لم يسموا أساليب واجهة المستخدم مباشرةً ، لكنهم أعادوا بعض الحالات العامة ، التي كانت فيها جميع حالات الشاشة الممكنة:


 compositeDisposable.add( Observable.merge(firstAction, secondAction) .observeOn(AndroidSchedulers.mainThread()) .subscribe(view.onChangeState)) return compositeDisposable 

 class SomeViewState(val progress: Boolean? = null, val error: Throwable? = null, val errorMessage: String? = error?.message, val result: TaskUi? = null) 

كان النشاط سليل واجهة SomeViewStateMachine:


 interface SomeViewStateMachine { fun toSuccess(task: SomeUiModel) fun toError(error: String?) fun toProgress() fun changeSomeButton(buttonEnabled: Boolean) } 

عندما نقر المستخدم على شيء ما على الشاشة ، حدث حدث في مقدم العرض وقام بإنشاء نموذج جديد ، تم رسمه بواسطة فئة خاصة:


 class SomeViewStateResolver(private val stateMachine: SomeViewStateMachine) : Consumer<SomeViewState> { override fun accept(stateUpdate: SomeViewState) { if (stateUpdate.result != null) { stateMachine.toSuccess(stateUpdate.result) } else if (stateUpdate.error != null && stateUpdate.progress == false) { stateMachine.toError(stateUpdate.errorMessage) } else if (stateUpdate.progress == true) { stateMachine.toProgress() } else if (stateUpdate.someButtonEnabled != null) { stateMachine.changeSomeButton(stateUpdate.someButtonEnabled) } } } 

توافق ، بعض MVP غريب ، وحتى بعيدا عن MVI. تبحث عن الإلهام.


الفصل 2. الإعادة



نتحدث عن مشاكله مع المطورين الآخرين ، علم قائدنا (بعد ذلك) سيرغي بويتشيان عن Redux .


بعد مشاهدة حديث Dorfman عن جميع التصميمات واللعب مع Redux ، قررنا استخدامه لتحسين تصميمنا المعماري.
ولكن أولاً ، دعونا نلقي نظرة فاحصة على الهندسة المعمارية وننظر في إيجابيات وسلبيات.


عمل
يصف العمل.


ActionCreator
إنه مثل محلل الأنظمة: التنسيقات ، يكمل مواصفات متطلبات العملاء حتى يفهمه المبرمجون.
عندما ينقر المستخدم على الشاشة ، يقوم ActionsCreator بتكوين إجراء ينتقل إلى البرامج الوسيطة (نوع من منطق الأعمال). يمنحنا منطق العمل بيانات جديدة يتلقاها المخفض ويسحبها.


إذا نظرت إلى الصورة مرة أخرى ، فقد تلاحظ كائنًا مثل المتجر. متجر المخازن المخفضات. بمعنى آخر ، نرى أن الأخوة المتقدمين - الأخوة المؤسفة - خمنوا أن كائنًا واحدًا كبيرًا يمكن نشره في العديد من الأشياء الصغيرة ، وكل منها سيكون مسؤولًا عن الجزء الخاص به من الشاشة. وهذه مجرد فكرة رائعة!


نموذج التعليمة البرمجية لـ ActionCreators بسيطة (دقيق ، JavaScript!):


 export function addTodo(text) { return { type: ADD_TODO, text } } export function toggleTodo(index) { return { type: TOGGLE_TODO, index } } export function setVisibilityFilter(filter) { return { type: SET_VISIBILITY_FILTER, filter } } 

المخفض


تصف الإجراءات حقيقة أن شيئًا ما قد حدث ، لكن لا يشير إلى كيفية تغيير حالة التطبيق استجابةً لذلك ، فهذا عمل لـ Reducer.

باختصار ، يعرف المخفض كيفية تحديث الشاشة / العرض بشكل متقطع.


الايجابيات:


  • تحديث الشاشة متحللة.
  • دفق البيانات أحادي الاتجاه.

سلبيات:


  • التبديل المفضل مرة أخرى.
     function todoApp(state = initialState, action) { switch (action.type) { case SET_VISIBILITY_FILTER: return Object.assign({}, state, { visibilityFilter: action.filter }) case ADD_TODO: return Object.assign({}, state, { todos: [ ...state.todos, { text: action.text, completed: false } ] }) default: return state } 
  • حفنة من كائنات الدولة.
  • فصل المنطق إلى ActionCreator و المخفض.

نعم ، بدا لنا أن الفصل بين ActionCreator و Reducer ليس هو الخيار الأفضل لتوصيل النموذج والشاشة ، لأن كتابة مثيل (is) هو نهج سيء. وهنا اخترعنا الهندسة المعمارية لدينا!


الفصل 3. EBA



ما هو Action و ActionCreator في سياق EBA:


 typealias Action = () -> Unit typealias ActionMapper<T> = (T) -> Action interface ActionCreator<T> : (T) -> (Observable<Action>) 

نعم ، نصف العمارة هي typealias وواجهة. البساطة تساوي الأناقة!


يلزم اتخاذ إجراء لاستدعاء شيء ما دون إرسال أي بيانات. نظرًا لأن ActionCreator يُرجع أحد الملاحظات ، فقد اضطررنا إلى التفاف Action في lambda آخر لنقل بعض البيانات. وهكذا اتضح ActionMapper - إجراء مكتوب يمكننا من خلاله تمرير ما نحتاجه لتحديث الشاشة / العرض.

المسلمات الأساسية:

واحد ActionCreator - جزء واحد من الشاشة

مع الفقرة الأولى ، كل شيء واضح: بحيث لا يوجد جهنم من التحديثات المتقاطعة غير المفهومة ، اتفقنا على أنه يمكن لـ ActionCreator تحديث جزءه من الشاشة فقط. إذا كانت قائمة ، فإنها تقوم بتحديث القائمة فقط ، وإذا كان الزر هو فقط.


ليست هناك حاجة خنجر

ولكن ، يتساءل المرء ، لماذا لم يرضنا خنجر؟ انا اقول لك
القصة النموذجية هي عندما يكون مجردة سيرجي ويعرف أيضا باسم خنجر سيد "ماذا يفعل هذا الملخص؟" هل في المشروع.


اتضح أنه إذا جربت خنجرًا ، فيجب عليك أن تشرح كل مرة لكل مطور جديد (وليس فقط جديد). أو ربما نسيت نفسك بالفعل ما يفعله هذا التعليق التوضيحي ، وتذهب إلى google.


كل هذا يعقد عملية إنشاء الميزات إلى حد كبير دون تقديم الكثير من الراحة. لذلك ، قررنا أننا سننشئ الأشياء التي نحتاجها بأيدينا ، لذلك سيكون تجميعها أسرع ، لأنه لا يوجد إنشاء كود. نعم ، سنقضي خمس دقائق إضافية في كتابة جميع التبعيات بأيدينا ، لكننا سنوفر الكثير من الوقت على التجميع. نعم ، لم نتخل عن الخنجر في كل مكان ، فهو يستخدم على المستوى العالمي ، ويخلق بعض الأشياء الشائعة ، لكننا نكتبها في جافا لتحسينها بشكل أفضل ، حتى لا نجذب kapt.


مخطط العمارة :



المكون هو تناظرية لنفس المكون من خنجر ، فقط دون خنجر. مهمته هي خلق الموثق. الموثق يربط ActionCreators معا. من أحداث العرض إلى الموثق تحدث الأحداث ، ومن الإجراءات الموثقة إلى المشاهدة ، يتم إرسال الإجراءات التي تقوم بتحديث الشاشة.


ActionCreator



الآن دعنا نرى أي نوع من الأشياء - ActionCreator. في أبسط الحالات ، فإنه ببساطة يعالج الإجراء أحادي الاتجاه. افترض أن هناك مثل هذا السيناريو: نقر المستخدم على زر "إنشاء مهمة". يجب فتح شاشة أخرى ، حيث سنصفها ، دون أي طلبات إضافية.


للقيام بذلك ، نحن ببساطة الاشتراك في الزر باستخدام RxBinding من جيك الحبيب لدينا وانتظر حتى يقوم المستخدم بالنقر فوقه. بمجرد حدوث نقرة ، سيرسل Binder الحدث إلى ActionCreator معين ، والذي سوف يسمي Action الخاص بنا ، والذي سيفتح شاشة جديدة لنا. لاحظ أنه لا توجد مفاتيح. بعد ذلك ، سأظهر في الكود سبب ذلك.
إذا احتجنا فجأة للذهاب إلى الشبكة أو قاعدة البيانات ، فنحن نوجه هذه الطلبات إلى هناك ، ولكن من خلال المتفاعلات التي قمنا بتمريرها إلى مُنشئ ActionCreator عبر الواجهة للاتصال بهم:


إخلاء المسئولية: تنسيق الكود ليس صحيحًا تمامًا هنا ، لديّ قواعده الخاصة بالمقال بحيث تتم قراءة الكود جيدًا.

 class LoadItemsActionCreator( private val getItems: () -> Observable<List<ViewTyped>>, private val showLoadedItems: ActionMapper<DiffResult<ViewTyped>>, private val diffCalculator: DiffCalculator<ViewTyped>, private val errorItem: ErrorView, private val emptyItem: ViewTyped? = null) : ActionOnEvent 

بكلمات "من خلال واجهة دعوتهم" قصدت بالضبط كيف يتم الإعلان عن getItems (هنا ViewTyped هو واجهة لدينا للعمل مع القوائم). بالمناسبة ، أعدنا استخدام ActionCreator في ثمانية أجزاء مختلفة من التطبيق ، لأنه مكتوب متعدد الاستخدامات قدر الإمكان.


نظرًا لأن الأحداث ذات طبيعة تفاعلية ، يمكننا تجميع سلسلة من خلال إضافة عوامل تشغيل أخرى هناك ، على سبيل المثال startWith (showLoadingAction) لإظهار التحميل ، و onErrorReturn (errorAction) لإظهار حالة الشاشة مع وجود خطأ.
وكل هذا تفاعلي!


مثال


 class AboutFragment : CompositionFragment(R.layout.fragment_about) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val component = AboutComponent( setVersionName = { { appVersion.text = it } }, openPdfAction = { (url, name) -> { openPdf(url, name) } }) val events = AboutEventsImpl( bindEvent = bindEvent, openPolicyPrivacyEvent = confidentialityPolicy.clicks(), openProcessingPersDataEvent = personalDataProtection.clicks(), unbindEvent = unBindEvent) component.binder().bind(events) } 

لننظر أخيرًا إلى الهيكل باستخدام الكود كمثال. للبدء ، اخترت واحدة من أبسط الشاشات - حول التطبيق ، لأنها شاشة ثابتة.
النظر في إنشاء مكون:


 val component = AboutComponent( setVersionName = { { appVersion.text = it } }, openPdfAction = { (url, name) -> { openPdf(url, name) } } ) 

وسيطات المكون - الإجراءات / ActionMappers - تساعد على ربط طريقة العرض بـ ActionCreators. في ActionMapper'e setVersionName ، نمرر إصدار المشروع ونخصص هذه القيمة للنص على الشاشة. في openPdfAction ، زوج من الارتباط بمستند واسم لفتح الشاشة التالية حيث يمكن للمستخدم قراءة هذا المستند.


هنا هو المكون نفسه:


 class AboutComponent( private val setVersionName: ActionMapper<String>, private val openPdfAction: ActionMapper<Pair<String, String>>) { fun binder(): AboutEventsBinder { val openPolicyPrivacy = OpenPdfActionCreator(openPdfAction, someUrlString) val openProcessingPersonalData = OpenPdfActionCreator(openPdfAction, anotherUrlString) val setVersionName = setVersionName.toSimpleActionCreator( moreComponent::currentVersionName ) return AboutEventsBinder(setVersionName, openPolicyPrivacy, openProcessingPersonalData) } } 

دعني أذكرك بما يلي:


 typealias Action = () -> Unit typealias ActionMapper<T> = (T) -> Action 

حسنا ، دعنا ننتقل.


 fun binder(): AboutEventsBinder 

دعونا نلقي نظرة على AboutEventsBinder بمزيد من التفاصيل.


 class AboutEventsBinder(private val setVersionName: ActionOnEvent, private val openPolicyPrivacy: ActionOnEvent, private val openProcessingPersonalData: ActionOnEvent) : BaseEventsBinder<AboutEvents>() { override fun bindInternal(events: AboutEvents): Observable<Action> { return Observable.merge( setVersionName(events.bindEvent), openPolicyPrivacy(events.openPolicyPrivacyEvent), openProcessingPersonalData(events.openProcessingPersDataEvent)) } } 

ActionOnEvent هو typealias آخر ، حتى لا تكتب في كل مرة.


 ActionCreator<Observable<*>> 

في AboutEventsBinder ، نقوم بتمرير ActionCreators ، وندعوهم ، لربط حدث معين. ولكن لفهم كيفية اتصال كل هذا ، دعونا ننظر إلى الفئة الأساسية - BaseEventsBinder.


 abstract class BaseEventsBinder<in EVENTS : BaseEvents>( private val uiScheduler: Scheduler = AndroidSchedulers.mainThread() ) { fun bind(events: EVENTS) { bindInternal(events).observeOn(uiScheduler) .takeUntil(events.unbindEvent) .subscribe(Action::invoke) } protected abstract fun bindInternal(events: EVENTS): Observable<Action> } 

نرى طريقة bindInternal المألوفة ، والتي قمنا بإعادة تعريفها في الخلف. الآن النظر في طريقة الربط. كل السحر هنا. نحن نقبل مورث واجهة BaseEvents ، ونمرره إلى bindInternal لتوصيل الأحداث والإجراءات. بمجرد أن نقول أن كل ما يأتي ، فإننا ننفذ على واجهة المستخدم والاشتراك. ونحن نرى أيضا الاختراق مثيرة للاهتمام - takeUntil.


 interface BaseEvents { val unbindEvent: EventObservable } 

بعد تحديد حقل unbindEvent في BaseEvents للتحكم في إلغاء الاشتراك ، يجب علينا تنفيذه في جميع الورثة. يتيح لك هذا الحقل الرائع إلغاء الاشتراك تلقائيًا من السلسلة بمجرد اكتمال هذا الحدث. إنه رائع فقط! الآن لا يمكنك متابعة ولا تقلق بشأن دورة الحياة والنوم بسلام.


 val openPolicyPrivacy = OpenPdfActionCreator(openPdfAction, policyPrivacyUrl) val openProcessingPersonalData = OpenPdfActionCreator(openPdfAction, personalDataUrl) 

العودة إلى المكون. وهنا يمكنك أن ترى بالفعل طريقة إعادة الاستخدام. لقد كتبنا فئة واحدة يمكنها فتح شاشة عرض ملف pdf ، ولا يهمنا ما هو عنوان url. لا رمز الازدواجية.


 class OpenPdfActionCreator( private val openPdfAction: ActionMapper<Pair<String, String>>, private val pdfUrl: String) : ActionOnEvent { override fun invoke(event: EventObservable): Observable<Action> { return event.map { openPdfAction(pdfUrl to pdfUrl.substringAfterLast(FILE_NAME_DELIMITER)) } } } 

إن أكشن ActionCreator بسيط أيضًا قدر الإمكان ، وهنا نؤدي بعض التلاعب بالسلسلة.


دعنا نعود إلى المكون وننظر في ActionCreator التالية:


 setVersionName.toSimpleActionCreator(moreComponent::currentVersionName) 

بمجرد أن أصبحنا كسولين جدًا في الكتابة عن ActionCreators نفسه وبسيط بطبيعته. استخدمنا قوة Kotlin وكتب extension'y. على سبيل المثال ، في هذه الحالة ، نحتاج فقط إلى تمرير سلسلة ثابتة إلى ActionMapper.


 fun <R> ActionMapper<R>.toSimpleActionCreator( mapper: () -> R): ActionCreator<Observable<*>> { return object : ActionCreator<Observable<*>> { override fun invoke(event: Observable<*>): Observable<Action> { return event.map { this@toSimpleActionCreator(mapper()) } } } } 

هناك أوقات لا نحتاج فيها إلى نقل أي شيء على الإطلاق ، ولكننا ندعو فقط بعض الإجراءات - على سبيل المثال ، لفتح الشاشة التالية:


 fun Action.toActionCreator(): ActionOnEvent { return object : ActionOnEvent { override fun invoke(event: EventObservable): Observable<Action> { return event.map { this@toActionCreator } } } } 

لذلك ، مع انتهاء المكون ، ارجع إلى الجزء:


 val events = AboutEventsImpl( bindEvent = bindEvent, openPolicyPrivacyEvent = confidentialityPolicy.throttleFirstClicks(), openProcessingPersDataEvent = personalDataProtection.throttleFirstClicks(), unbindEvent = unBindEvent) 

هنا نرى إنشاء فئة مسؤولة عن تلقي الأحداث من المستخدم. كما أن التوثيق والربط هما مجرد شاشة لأحداث دورة الحياة التي نلتقطها باستخدام مكتبة Trello's Navi.


 fun <T> NaviComponent.observe(event: Event<T>): Observable<T> = RxNavi.observe(this, event) val unBindEvent: Observable<*> = observe(Event.DESTROY_VIEW) val bindEvent: Observable<*> = Observable.just(true)  val bindEvent = observe(Event.POST_CREATE) 

تصف واجهة الأحداث أحداث شاشة معينة ، بالإضافة إلى أنها يجب أن ترث BaseEvents. ما يلي هو دائما تنفيذ واجهة. في هذه الحالة ، تحولت الأحداث إلى واحدة مع تلك التي تأتي من الشاشة ، ولكن يحدث أن تحتاج إلى الاحتفاظ بحدثين معًا.


على سبيل المثال ، يجب دمج أحداث تحميل الشاشة عند الفتح وإعادة التحميل في حالة حدوث خطأ في واحدة - فقط قم بتحميل الشاشة.


 interface AboutEvents : BaseEvents { val bindEvent: EventObservable val openPolicyPrivacyEvent: EventObservable val openProcessingPersDataEvent: EventObservable } class AboutEventsImpl(override val bindEvent: EventObservable, override val openPolicyPrivacyEvent: EventObservable, override val openProcessingPersDataEvent: EventObservable, override val unbindEvent: EventObservable) : AboutEvents 

نعود إلى الشظية ونجمع كل شيء معًا! نطلب من المكون إنشاء الموثق وإعادته إلينا ، ثم نطلق عليه طريقة الربط ، حيث نمر الكائن الذي يشاهد أحداث الشاشة.


 component.binder().bind(events) 

لقد تم كتابة مشروع على هذا الهيكل لمدة عامين تقريبا الآن. وليس هناك حد لسعادة المديرين في سرعة مشاركة الميزات! ليس لديهم وقت للتوصل إلى وقت جديد ، لأننا انتهينا بالفعل من القديم. الهيكل مرن للغاية ويسمح لك بإعادة استخدام الكثير من التعليمات البرمجية.
يمكن أن يسمى عيب هذه الهندسة المعمارية عدم الحفاظ على الدولة. ليس لدينا نموذج كامل يصف حالة الشاشة ، كما في MVI ، ولكن يمكننا التعامل معها. مثل - انظر أدناه.


الفصل 4. مكافأة


أعتقد أن كل شخص يعرف مشكلة التحليلات: لا أحد يحب أن يكتبها ، لأنه يزحف عبر جميع الطبقات ويزيل التحديات. منذ بعض الوقت ، وكان علينا مواجهته. ولكن بفضل الهندسة المعمارية لدينا ، تم الحصول على تنفيذ جميل جدا.


لذا ، ما كانت فكرتي: عادة ما تترك التحليلات استجابة لإجراءات المستخدم. ولدينا فقط فئة تتراكم إجراءات المستخدم. حسنًا ، لنبدأ.


الخطوة 1 نقوم بتغيير الفئة الأساسية BaseEventsBinder قليلاً عن طريق التفاف الأحداث في trackAnalytics:


 abstract class BaseEventsBinder<in EVENTS : BaseEvents>( private val trackAnalytics: TrackAnalytics<EVENTS> = EmptyAnalyticsTracker(), private val uiScheduler: Scheduler = AndroidSchedulers.mainThread()) { @SuppressLint("CheckResult") fun bind(events: EVENTS) { bindInternal(trackAnalytics(events)).observeOn(uiScheduler) .takeUntil(events.unbindEvent) .subscribe(Action::invoke) } protected abstract fun bindInternal(events: EVENTS): Observable<Action> } 

الخطوة 2 ننشئ تطبيقًا ثابتًا لمتغير trackAnalytics من أجل الحفاظ على التوافق مع الإصدارات السابقة وعدم كسر الورثة الذين لا يحتاجون إلى تحليلات حتى الآن:


 interface TrackAnalytics<EVENTS : BaseEvents> { operator fun invoke(events: EVENTS): EVENTS } class EmptyAnalyticsTracker<EVENTS : BaseEvents> : TrackAnalytics<EVENTS> { override fun invoke(events: EVENTS): EVENTS = events } 

الخطوة 3 نكتب تنفيذ واجهة TrackAnalytics للشاشة المرغوبة - على سبيل المثال ، لشاشة قائمة المشروع:


 class TrackProjectsEvents : TrackAnalytics<ProjectsEvents> { override fun invoke(events: ProjectsEvents): ProjectsEvents { return object : ProjectsEvents by events { override val boardClickEvent = events.boardClickEvent.trackTypedEvent { allProjectsProjectClick(it.title) } override val openBoardCreationEvent = events.openBoardCreationEvent.trackEvent { allProjectsAddProjectClick() } override val openCardsSearchEvent = events.openCardsSearchEvent.trackEvent { allProjectsSearchBarClick() } } } } 

هنا نستخدم مرة أخرى قوة Kotlin في شكل مندوبين. لدينا بالفعل واجهة وراثة أنشأناها - في هذه الحالة ، ProjectEvents. لكن بالنسبة لبعض الأحداث ، تحتاج إلى إعادة تعريف كيفية سير الأحداث وإضافة ارتباط حولها بإرسال التحليلات. في الواقع ، trackEvent هو doOnNext فقط:


 inline fun <T> Observable<T>.trackEvent(crossinline event: AnalyticsSpec.() -> Unit): Observable<T> = doOnNext { event(analyticsSpec) } inline fun <T> Observable<T>.trackTypedEvent(crossinline event: AnalyticsSpec.(T) -> Unit): Observable<T> = doOnNext { event(analyticsSpec, it) } 

الخطوة 4 يبقى لنقل هذا إلى بيندر. نظرًا لأننا نصنعه في مكون ، فلدينا الفرصة ، إذا كنت بحاجة فجأة ، لإضافة تبعيات إضافية إلى المُنشئ. الآن سيبدو مُنشئ ProjectsEventsBinder بالشكل التالي:


 class ProjectsEventsBinder( private val loadItems: LoadItemsActionCreator, private val refreshBoards: ActionOnEvent, private val openBoard: ActionCreator<Observable<BoardId>>, private val openScreen: ActionOnEvent, private val openCardSearch: ActionOnEvent, trackAnalytics: TrackAnalytics<ProjectsEvents>) : BaseEventsBinder<ProjectsEvents>(trackAnalytics) 

يمكنك إلقاء نظرة على أمثلة أخرى على جيثب .


سؤال وجواب


كيف يمكنك الحفاظ على حالة الشاشة؟

لا مفر نحن كتلة التوجه. لكننا نستخدم أيضًا الوسيطات / القصد ونحفظ متغير OPENED_FROM_BACKSTACK هناك. وعند تصميم Binder ، فإننا ننظر إليه. إذا كان غير صحيح - تحميل البيانات من الشبكة. إذا كان ذلك صحيحا - من ذاكرة التخزين المؤقت. هذا يسمح لك بإعادة إنشاء الشاشة بسرعة.


لكل من يعارض حظر التوجيه: حاول اختبار التحليلات وإيداعها بشأن عدد المرات التي يدير فيها المستخدمون هاتفك وعددهم في اتجاه مختلف. النتائج قد مفاجأة.


لا أرغب في كتابة مكونات ، كيف يمكنني تكوين صداقات مع الخنجر؟

لا أنصح ، ولكن إذا كنت لا تمانع في تجميع الوقت ، يمكنك إنشاء مكون من خلال خنجر أيضًا. لكننا لم نحاول.


أنا لا أكتب في kotlin ، ما هي الصعوبات في تنفيذ جافا؟

كل ذلك يمكن كتابته بلغة جافا ، ولن يبدو جميلًا جدًا.


إذا أعجبك المقال ، فسيتم شرح الجزء التالي حول كيفية كتابة الاختبارات على مثل هذه البنية (عندها سيتضح سبب وجود العديد من الواجهات). Spoiler - الكتابة سهلة ويمكنك الكتابة على جميع الطبقات باستثناء المكون ، لكنك لست بحاجة إلى اختباره ، فهو فقط ينشئ كائنًا موثقًا.


شكرًا لزملائك من فريق تطوير الأجهزة المحمولة Tinkoff Business على مساعدتهم في كتابة هذا المقال.

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


All Articles