Ich bin vor ein paar Jahren zu Tinkoff gekommen, und zwar wegen eines neuen Projekts, Kunden und Projekte , das gerade in den Startlöchern war.
Jetzt kann ich mich nicht mehr an meine Gefühle aus der damals neuen Architektur erinnern. Aber ich erinnere mich sicher: Es war ungewöhnlich, dass Rx außerhalb der üblichen Fahrten zum Netzwerk und zur Basis woanders verwendet wird. Jetzt, da diese Architektur bereits einen evolutionären Entwicklungspfad durchlaufen hat, möchte ich endlich darüber sprechen, was passiert ist und was dazu gekommen ist.

Meiner Meinung nach sind alle derzeit gängigen Architekturen - MVP, MVVM und sogar MVI - schon lange in der Arena und nicht immer verdient. Haben sie keine Mängel? Ich sehe viele von ihnen. Wir haben an unserer Stelle entschieden, dass es ausreicht, um es auszuhalten, und haben eine neue, asynchrone Architektur (neu) erfunden.
Ich werde kurz beschreiben, was mir an aktuellen Architekturen nicht gefällt. Einige Punkte können kontrovers sein. Vielleicht haben Sie das noch nie erlebt, Sie schreiben perfekte und allgemein Jedi-Programmierung. Dann vergib mir, ein Sünder.
Also mein Schmerz ist:
- Riesiger Moderator / ViewModel.
- Eine riesige Menge an Switch-Case in MVI.
- Die Unfähigkeit, Teile von Presenter / ViewModel wiederzuverwenden, und folglich die Notwendigkeit, Code zu duplizieren.
- Viele veränderbare Variablen, die von überall geändert werden können. Dementsprechend ist ein solcher Code schwierig zu warten und zu modifizieren.
- Nicht zerlegte Bildschirmaktualisierung.
- Es ist schwer, Tests zu schreiben.
Problem
Zu jedem Zeitpunkt hat die Anwendung einen bestimmten Status, der ihr Verhalten und das, was der Benutzer sieht, definiert. Dieser Status enthält alle Werte von Variablen - von einfachen Flags bis zu einzelnen Objekten. Jede dieser Variablen lebt ihr eigenes Leben und wird von verschiedenen Teilen des Codes gesteuert. Sie können den aktuellen Status der Anwendung nur ermitteln, indem Sie alle nacheinander überprüfen.
Ein Artikel zur modernen Kotlin MVI-Architektur
Kapitel 1. Evolution ist unser Alles
Anfangs haben wir über MVP geschrieben, aber ein wenig mutiert. Es war eine Mischung aus MVP und MVI. Es gab Entitäten von MVP in Form eines Presenters und einer View-Oberfläche:
interface NewTaskView { val newTaskAction: Observable<NewTaskAction> val taskNameChangeAction: Observable<String> val onChangeState: Consumer<SomeViewState> }
Bereits hier können Sie den Haken bemerken: Ansicht hier ist sehr weit von den Kanonen von MVP entfernt. Es gab eine Methode im Moderator:
fun bind(view: SomeView): Disposable
Draußen wurde eine Schnittstellenimplementierung übergeben, die Änderungen an der Benutzeroberfläche reaktiv abonnierte. Und es riecht schon nach MVI!
Mehr ist mehr. In Presenter wurden verschiedene Interaktoren erstellt und abonniert, die die Ansichtsänderungen jedoch nicht direkt aufriefen, sondern einen globalen Status zurückgaben, in dem alle möglichen Bildschirmzustände vorhanden waren:
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)
Die Aktivität war der Nachkomme der SomeViewStateMachine-Schnittstelle:
interface SomeViewStateMachine { fun toSuccess(task: SomeUiModel) fun toError(error: String?) fun toProgress() fun changeSomeButton(buttonEnabled: Boolean) }
Wenn der Benutzer auf etwas auf dem Bildschirm klickte, kam ein Ereignis in den Präsentator und er erstellte ein neues Modell, das von einer speziellen Klasse gezeichnet wurde:
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) } } }
Stimmen Sie zu, ein seltsamer MVP und sogar weit weg von MVI. Auf der Suche nach Inspiration.
Kapitel 2. Redux

Als unser (damals noch) Chef Sergey Boishtyan über seine Probleme mit anderen Entwicklern sprach, erfuhr er von Redux .
Nachdem wir Dorfmans Vortrag über alle Architekturen gesehen und mit Redux gespielt hatten , beschlossen wir, ihn zur Aktualisierung unserer Architektur zu verwenden.
Aber schauen wir uns zuerst die Architektur genauer an und betrachten ihre Vor- und Nachteile.
Aktion
Beschreibt die Aktion.
Actioncreator
Er ist wie ein Systemanalytiker: Formate, ergänzt die Kundenanforderungsspezifikation, damit Programmierer ihn verstehen.
Wenn der Benutzer auf den Bildschirm klickt, bildet ActionsCreator eine Aktion, die zur Middleware wechselt (eine Art Geschäftslogik). Die Geschäftslogik gibt uns neue Daten, die ein bestimmter Reduzierer empfängt und zeichnet.
Wenn Sie sich das Bild noch einmal ansehen, bemerken Sie möglicherweise ein Objekt wie Speichern. Store speichert Reduzierer. Das heißt, wir sehen, dass die Front-End-Brüder - unglückliche Brüder - vermutet haben, dass ein großes Objekt in viele kleine zersägt werden kann, von denen jedes für seinen eigenen Teil des Bildschirms verantwortlich ist. Und das ist einfach ein wunderbarer Gedanke!
Beispielcode für einfache ActionCreators (vorsichtig, 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 } }
Reduzierstück
Aktionen beschreiben die Tatsache, dass etwas passiert ist, geben jedoch nicht an, wie sich der Status der Anwendung als Reaktion ändern soll. Dies funktioniert für Reducer.
Kurz gesagt, Reducer weiß, wie der / view-Bildschirm zerlegt aktualisiert wird.
Vorteile:
- Zerlegte Bildschirmaktualisierung.
- Unidirektionaler Datenstrom.
Nachteile:
- Lieblingsschalter wieder.
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 }
- Eine Reihe von Staatsobjekten.
- Trennung der Logik in ActionCreator und Reducer.
Ja, es schien uns, dass die Trennung von ActionCreator und Reducer nicht die beste Option ist, um das Modell und den Bildschirm zu verbinden, da das Schreiben einer Instanz von (is) ein schlechter Ansatz ist. Und hier haben wir UNSERE Architektur erfunden!
Kapitel 3. EBA

Was ist Action und ActionCreator im Kontext von EBA:
typealias Action = () -> Unit typealias ActionMapper<T> = (T) -> Action interface ActionCreator<T> : (T) -> (Observable<Action>)
Ja, die Hälfte der Architektur besteht aus Typealias und einer Schnittstelle. Einfachheit ist gleich Eleganz!
Es sind Maßnahmen erforderlich, um etwas aufzurufen, ohne Daten zu übertragen. Da ActionCreator ein Observable zurückgibt, mussten wir Action in ein anderes Lambda einbinden, um einige Daten zu übertragen. Und so stellte sich heraus, dass ActionMapper eine typisierte Aktion ist, durch die wir alles übergeben können, was wir zum Aktualisieren des Bildschirms / der Ansicht benötigen.
Grund Postulate:
Ein ActionCreator - ein Teil des BildschirmsMit dem ersten Absatz ist alles klar: Damit unverständliche Cross-Updates nichts anhaben können, haben wir uns darauf geeinigt, dass ein ActionCreator nur seinen Teil des Bildschirms aktualisieren kann. Wenn es sich um eine Liste handelt, wird nur die Liste aktualisiert, wenn nur die Schaltfläche angezeigt wird.
Dolch wird nicht benötigtAber man fragt sich, warum hat uns Dolch nicht gefallen? Ich sage es dir.
Eine typische Geschichte ist, wenn ein abstrakter Sergey alias Dolchmeister alias „Was macht dieser abstrakte?“ Am Projekt ist.
Es stellt sich heraus, dass Sie, wenn Sie mit einem Dolch experimentiert haben, jedes Mal jedem neuen (und nicht nur neuen) Entwickler erklären müssen. Oder vielleicht haben Sie selbst bereits vergessen, was diese Anmerkung bewirkt, und Sie gehen auf Google.
All dies erschwert das Erstellen von Features erheblich, ohne viel Komfort zu bieten. Aus diesem Grund haben wir uns entschlossen, die benötigten Elemente mit unseren Händen zu erstellen, damit der Zusammenbau schneller vonstatten geht, da keine Codegenerierung erfolgt. Ja, wir werden zusätzliche fünf Minuten damit verbringen, alle Abhängigkeiten mit unseren Händen zu schreiben, aber wir werden viel Zeit beim Kompilieren sparen. Ja, wir haben den Dolch nicht überall aufgegeben, er wird auf globaler Ebene verwendet, er schafft einige gemeinsame Dinge, aber wir schreiben sie zur besseren Optimierung in Java, um kapt nicht anzulocken.
Architekturschema :

Komponente ist ein Analogon derselben Komponente von Dagger, nur ohne Dolch. Seine Aufgabe ist es, einen Ordner zu erstellen. Binder bindet ActionCreators zusammen. Von Ansicht zu Ordner Ereignisse werden durch Ereignisse ausgelöst, und von Ordner zu Ansicht werden Aktionen gesendet, die den Bildschirm aktualisieren.
Actioncreator

Nun wollen wir sehen, was das ist - ActionCreator. Im einfachsten Fall wird die Aktion einfach unidirektional verarbeitet. Angenommen, es gibt ein solches Szenario: Der Benutzer hat auf die Schaltfläche "Aufgabe erstellen" geklickt. Ein weiterer Bildschirm sollte sich öffnen, wo wir ihn beschreiben werden, ohne zusätzliche Anforderungen.
Dazu abonnieren wir einfach den Button mit RxBinding von unserem geliebten Jake und warten, bis der Benutzer darauf klickt. Sobald ein Klick erfolgt, sendet Binder das Ereignis an einen bestimmten ActionCreator, der unsere Aktion aufruft und einen neuen Bildschirm für uns öffnet. Beachten Sie, dass es keine Schalter gab. Als nächstes werde ich im Code zeigen, warum das so ist.
Wenn wir plötzlich zum Netzwerk oder zur Datenbank gehen müssen, stellen wir diese Anforderungen genau dort, aber über die Interaktoren, die wir über die Schnittstelle an den ActionCreator-Konstruktor übergeben haben, um sie aufzurufen:
Haftungsausschluss: Die Formatierung des Codes stimmt hier nicht ganz, ich habe seine Regeln für den Artikel, damit der Code gut gelesen wird.
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
Mit den Worten "über die Schnittstelle ihres Aufrufs" habe ich genau gemeint, wie getItems deklariert wird (hier ist ViewTyped unsere Schnittstelle zum Arbeiten mit Listen). Übrigens haben wir diesen ActionCreator in acht verschiedenen Teilen der Anwendung wiederverwendet, da er so vielseitig wie möglich geschrieben ist.
Da Ereignisse reaktiver Natur sind, können wir eine Kette zusammenstellen, indem wir andere Operatoren hinzufügen, z. B. startWith (showLoadingAction), um das Laden anzuzeigen, und onErrorReturn (errorAction), um einen Bildschirmstatus mit einem Fehler anzuzeigen.
Und das alles ist reaktiv!
Beispiel
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) }
Schauen wir uns zum Schluss die Architektur am Beispiel von Code an. Zu Beginn habe ich einen der einfachsten Bildschirme ausgewählt - über die Anwendung, da es sich um einen statischen Bildschirm handelt.
Erwägen Sie das Erstellen einer Komponente:
val component = AboutComponent( setVersionName = { { appVersion.text = it } }, openPdfAction = { (url, name) -> { openPdf(url, name) } } )
Komponentenargumente - Actions / ActionMappers - helfen dabei, View mit ActionCreators zu verknüpfen. In ActionMapper'e setVersionName übergeben wir die Version des Projekts und weisen diesen Wert dem Text auf dem Bildschirm zu. In openPdfAction ein Linkpaar zu einem Dokument und ein Name, um den nächsten Bildschirm zu öffnen, in dem der Benutzer dieses Dokument lesen kann.
Hier ist die Komponente selbst:
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) } }
Ich möchte Sie daran erinnern, dass:
typealias Action = () -> Unit typealias ActionMapper<T> = (T) -> Action
OK, lass uns weitermachen.
fun binder(): AboutEventsBinder
Schauen wir uns AboutEventsBinder genauer an.
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 ist ein weiterer Typealias, um nicht jedes Mal zu schreiben.
ActionCreator<Observable<*>>
In AboutEventsBinder übergeben wir ActionCreators und binden sie beim Aufrufen an ein bestimmtes Ereignis. Um zu verstehen, wie all dies zusammenhängt, schauen wir uns die Basisklasse BaseEventsBinder an.
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> }
Wir sehen die bekannte bindInternal-Methode, die wir im Nachfolger neu definiert haben. Betrachten Sie nun die Bindemethode. Die ganze Magie ist hier. Wir akzeptieren den Erben der BaseEvents-Schnittstelle und übergeben ihn an bindInternal, um Ereignisse und Aktionen zu verbinden. Sobald wir das sagen, was auch immer kommt, führen wir den UI-Stream aus und abonnieren ihn. Wir sehen auch einen interessanten Hack - takeUntil.
interface BaseEvents { val unbindEvent: EventObservable }
Nachdem wir das Feld unbindEvent in BaseEvents definiert haben, um das Abbestellen zu steuern, müssen wir es in allen Erben implementieren. In diesem wunderbaren Feld können Sie sich automatisch von der Kette abmelden, sobald dieses Ereignis abgeschlossen ist. Es ist einfach toll! Jetzt können Sie nicht folgen und sorgen sich nicht um den Lebenszyklus und schlafen friedlich.
val openPolicyPrivacy = OpenPdfActionCreator(openPdfAction, policyPrivacyUrl) val openProcessingPersonalData = OpenPdfActionCreator(openPdfAction, personalDataUrl)
Zurück zur Komponente. Und hier sehen Sie bereits die Methode der Wiederverwendung. Wir haben eine Klasse geschrieben, die den PDF-Bildschirm öffnen kann, und es ist uns egal, um welche URL es sich handelt. Keine Codeduplizierung.
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)) } } }
Der ActionCreator-Code ist auch so einfach wie möglich. Hier führen wir nur einige Zeichenfolgenmanipulationen durch.
Kehren wir zur Komponente zurück und betrachten den folgenden ActionCreator:
setVersionName.toSimpleActionCreator(moreComponent::currentVersionName)
Einmal wurden wir zu faul, um dieselben und von Natur aus einfachen ActionCreators zu schreiben. Wir haben die Kraft von Kotlin genutzt und die Erweiterung geschrieben. In diesem Fall mussten wir beispielsweise nur eine statische Zeichenfolge an ActionMapper übergeben.
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()) } } } }
Es gibt Zeiten, in denen wir überhaupt nichts senden müssen, sondern nur eine Aktion aufrufen, um beispielsweise den folgenden Bildschirm zu öffnen:
fun Action.toActionCreator(): ActionOnEvent { return object : ActionOnEvent { override fun invoke(event: EventObservable): Observable<Action> { return event.map { this@toActionCreator } } } }
Kehren Sie mit der Komponente zum Fragment zurück:
val events = AboutEventsImpl( bindEvent = bindEvent, openPolicyPrivacyEvent = confidentialityPolicy.throttleFirstClicks(), openProcessingPersDataEvent = personalDataProtection.throttleFirstClicks(), unbindEvent = unBindEvent)
Hier sehen wir die Erstellung einer Klasse, die für den Empfang von Ereignissen vom Benutzer verantwortlich ist. Und das Aufheben und Binden sind nur Bildschirmlebenszyklusereignisse, die wir mithilfe der Navi-Bibliothek von Trello erfassen.
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)
Die Ereignisschnittstelle beschreibt die Ereignisse eines bestimmten Bildschirms und muss BaseEvents erben. Das Folgende ist immer eine Implementierung der Schnittstelle. In diesem Fall stellte sich heraus, dass die Ereignisse eins zu eins mit den Ereignissen auf dem Bildschirm waren. Es kommt jedoch vor, dass Sie zwei Ereignisse zusammenhalten müssen.
Zum Beispiel sollten Ereignisse des Bildschirmladens beim Öffnen und erneuten Laden im Fehlerfall zu einem zusammengefasst werden - nur das Laden des Bildschirms.
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
Wir kehren zum Fragment zurück und kombinieren alles miteinander! Wir bitten die Komponente, den Ordner zu erstellen und an uns zurückzugeben, und rufen dann die Bindemethode auf, bei der wir das Objekt übergeben, das die Bildschirmereignisse überwacht.
component.binder().bind(events)
Wir schreiben seit ungefähr zwei Jahren ein Projekt zu dieser Architektur. Und das Glück der Manager in Bezug auf die Geschwindigkeit des Feature-Sharing ist unbegrenzt! Sie haben keine Zeit, sich eine neue auszudenken, da wir die alte bereits fertigstellen. Die Architektur ist sehr flexibel und ermöglicht es Ihnen, viel Code wiederzuverwenden.
Der Nachteil dieser Architektur kann als Nichtkonservierung des Zustands bezeichnet werden. Wir haben kein ganzes Modell, das den Status des Bildschirms beschreibt, wie in MVI, aber wir können damit umgehen. Like - siehe unten.
Kapitel 4. Bonus
Ich denke, jeder kennt das Problem der Analytik: Niemand schreibt es gerne, weil es durch alle Ebenen kriecht und Herausforderungen entstellt. Vor einiger Zeit, und wir mussten uns dem stellen. Dank unserer Architektur konnte jedoch eine sehr schöne Umsetzung erzielt werden.
Also, was war meine Idee: Analytics geht normalerweise als Reaktion auf Benutzeraktionen. Und wir haben nur eine Klasse, die Benutzeraktionen sammelt. Ok, lass uns anfangen.
Schritt 1 Wir ändern die BaseEventsBinder-Basisklasse geringfügig, indem wir Ereignisse in trackAnalytics einschließen:
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> }
Schritt 2 Wir erstellen eine stabile Implementierung der Variablen trackAnalytics, um die Abwärtskompatibilität aufrechtzuerhalten und die Erben, die noch keine Analyse benötigen, nicht zu brechen:
interface TrackAnalytics<EVENTS : BaseEvents> { operator fun invoke(events: EVENTS): EVENTS } class EmptyAnalyticsTracker<EVENTS : BaseEvents> : TrackAnalytics<EVENTS> { override fun invoke(events: EVENTS): EVENTS = events }
Schritt 3 Wir schreiben die Implementierung der TrackAnalytics-Oberfläche für den gewünschten Bildschirm - zum Beispiel für den Projektlistenbildschirm:
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() } } } }
Auch hier nutzen wir die Macht von Kotlin in Form von Delegierten. Wir haben bereits einen von uns erstellten Schnittstellenvererb - in diesem Fall ProjectsEvents. Bei einigen Ereignissen müssen Sie jedoch den Ablauf von Ereignissen neu definieren und beim Senden von Analysen eine Bindung hinzufügen. Tatsächlich ist trackEvent nur 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) }
Schritt 4 Dies muss noch an Binder weitergegeben werden. Da wir es in einer Komponente erstellen, haben wir die Möglichkeit, dem Konstruktor bei Bedarf plötzlich zusätzliche Abhängigkeiten hinzuzufügen. Jetzt sieht der ProjectsEventsBinder-Konstruktor folgendermaßen aus:
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)
Sie können sich andere Beispiele auf GitHub ansehen.
Fragen und Antworten
Wie behält man den Bildschirmstatus bei?Auf keinen Fall. Wir blockieren die Orientierung. Wir verwenden aber auch Argumente / Intent und speichern dort die Variable OPENED_FROM_BACKSTACK. Und wenn wir Binder entwerfen, sehen wir uns das an. Wenn dies falsch ist, laden Sie die Daten aus dem Netzwerk. Wenn wahr - aus dem Cache. Auf diese Weise können Sie den Bildschirm schnell neu erstellen.
Für alle, die gegen das Blockieren der Ausrichtung sind: Versuchen Sie, Analysen zu testen und zu hinterlegen, wie oft Ihre Benutzer das Telefon umdrehen und wie viele sich in einer anderen Ausrichtung befinden. Die Ergebnisse können überraschen.
Ich möchte keine Komponenten schreiben. Wie kann ich mich mit dem Dolch anfreunden?Ich rate nicht, aber wenn Ihnen die Kompilierungszeit nichts ausmacht, können Sie die Komponente auch mit einem Dolch erstellen. Aber wir haben es nicht versucht.
Ich schreibe nicht in Kotlin. Was sind die Schwierigkeiten bei der Implementierung in Java?Trotzdem kann in Java geschrieben werden, nur wird es nicht so schön aussehen.
Wenn Ihnen der Artikel gefällt, wird im nächsten Teil erläutert, wie Sie Tests für eine solche Architektur schreiben (dann wird klar, warum es so viele Schnittstellen gibt). Spoiler - Das Schreiben ist einfach und Sie können auf allen Ebenen außer der Komponente schreiben, müssen es jedoch nicht testen, sondern erstellen lediglich ein Binderobjekt.
Vielen Dank an die Kollegen vom Tinkoff Business Mobile Development Team für ihre Hilfe beim Schreiben dieses Artikels.