Aufbau eines reaktiven Komponentensystems mit Kotlin



Hallo allerseits! Mein Name ist Anatoly Varivonchik, ich bin ein Android-Entwickler bei Badoo. Heute werde ich Ihnen die Übersetzung des zweiten Teils des Artikels von meinem Kollegen Zsolt Kocsi über die Implementierung von MVI mitteilen, die wir täglich im Entwicklungsprozess verwenden. Der erste Teil ist hier .

Was wir wollen und wie wir es machen


Im ersten Teil des Artikels haben wir Features vorgestellt , die zentralen Elemente von MVICore , die wiederverwendet werden können. Sie können die einfachste Struktur haben und nur einen Reduzierer enthalten , oder sie können ein voll funktionsfähiges Werkzeug zum Verwalten von asynchronen Aufgaben, Ereignissen und vielem mehr werden.

Jedes Feature ist nachvollziehbar - es besteht die Möglichkeit, Änderungen in seinem Status zu abonnieren und Benachrichtigungen darüber zu erhalten. In diesem Fall kann Feature die Eingabequelle abonnieren. Und das ist sinnvoll, denn mit der Aufnahme von Rx in die Codebasis haben wir bereits viele beobachtbare Objekte und Abonnements auf verschiedenen Ebenen.

Im Zusammenhang mit der Zunahme der Anzahl reaktiver Komponenten ist es an der Zeit, darüber nachzudenken, was wir haben und ob es möglich ist, das System noch besser zu machen.

Wir müssen drei Fragen beantworten:

  1. Welche Elemente sollten beim Hinzufügen neuer reaktiver Komponenten verwendet werden?
  2. Was ist der einfachste Weg, um Ihre Abonnements zu verwalten?
  3. Ist es möglich, das Lebenszyklusmanagement / die Notwendigkeit, Abonnements zu löschen, zu ignorieren, um Speicherlecks zu vermeiden? Mit anderen Worten, können wir die Komponentenbindung von der Abonnementverwaltung trennen?

In diesem Teil des Artikels werden wir die Grundlagen und Vorteile des Aufbaus eines Systems mit reaktiven Komponenten untersuchen und sehen, wie Kotlin dabei hilft.

Hauptelemente


Als wir uns mit dem Design und der Standardisierung unserer Features befassten , hatten wir bereits viele verschiedene Ansätze ausprobiert und beschlossen, dass die Features in Form von reaktiven Komponenten vorliegen würden. Zunächst haben wir uns auf die Hauptschnittstellen konzentriert. Zunächst mussten wir die Arten der Eingabe- und Ausgabedaten bestimmen.

Wir haben wie folgt argumentiert:

  • Lassen Sie uns das Rad nicht neu erfinden - lassen Sie uns sehen, welche Schnittstellen bereits vorhanden sind.
  • Da wir die RxJava-Bibliothek bereits verwenden, ist es sinnvoll, auf die grundlegenden Schnittstellen zu verweisen.
  • Die Anzahl der Schnittstellen sollte minimiert werden.

Aus diesem Grund haben wir uns entschieden, ObservableSource <T> für die Ausgabe und Consumer <T> für die Eingabe zu verwenden. Warum nicht Observable / Observer ? Observable ist eine abstrakte Klasse, von der Sie erben müssen, und ObservableSource ist die von Ihnen implementierte Schnittstelle, die die Notwendigkeit der Implementierung eines reaktiven Protokolls vollständig erfüllt.

package io.reactivex; import io.reactivex.annotations.*; /** * Represents a basic, non-backpressured {@link Observable} source base interface, * consumable via an {@link Observer}. * * @param <T> the element type * @since 2.0 */ public interface ObservableSource<T> { /** * Subscribes the given Observer to this ObservableSource instance. * @param observer the Observer, not null * @throws NullPointerException if {@code observer} is null */ void subscribe(@NonNull Observer<? super T> observer); } 

Observer , die erste Schnittstelle, die mir in den Sinn kommt, implementiert vier Methoden: onSubscribe, onNext, onError und onComplete. Um das Protokoll so weit wie möglich zu vereinfachen, haben wir Consumer <T> bevorzugt, das neue Elemente mit einer einzigen Methode akzeptiert. Wenn wir Observer wählen , sind die verbleibenden Methoden meistens redundant oder funktionieren anders (zum Beispiel möchten wir Fehler als Teil des Status und nicht als Ausnahmen darstellen und den Stream sicherlich nicht unterbrechen).

 /** * A functional interface (callback) that accepts a single value. * @param <T> the value type */ public interface Consumer<T> { /** * Consume the given value. * @param t the value * @throws Exception on error */ void accept(T t) throws Exception; } 

Wir haben also zwei Schnittstellen, von denen jede eine Methode enthält. Jetzt können wir sie binden, indem wir Consumer <T> an ObservableSource <T> signieren. Letzteres akzeptiert nur Instanzen von Observer <T> , aber wir können es in ein Observable <T> einschließen , das Consumer <T> abonniert hat:

 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) 

(Glücklicherweise erstellt die .wrap (Ausgabe) -Funktion kein neues Objekt, wenn die Ausgabe bereits ein Observable <T> ist. )

Sie können sich daran erinnern, dass die Feature- Komponente aus dem ersten Teil des Artikels Eingabedaten vom Typ Wish (entsprechend Intent von Model-View-Intent) und Ausgaben vom Typ State verwendet hat und sich daher auf beiden Seiten des Bundles befinden kann:

 // Wishes -> Feature val wishes: ObservableSource<Wish> = Observable.just(Wish.SomeWish) val feature: Consumer<Wish> = SomeFeature() val disposable = Observable.wrap(wishes).subscribe(feature) // Feature -> State consumer val feature: ObservableSource<State> = SomeFeature() val logger: Consumer<State> = Consumer { System.out.println(it) } val disposable = Observable.wrap(feature).subscribe(logger) 

Diese Verknüpfung von Consumer und Producer sieht bereits recht einfach aus, aber es gibt eine noch einfachere Möglichkeit, Abonnements nicht manuell zu erstellen oder zu kündigen.

Binder vorstellen .

Steroidbindung


MVICore enthält eine Klasse namens Binder , die eine einfache API zum Verwalten von Rx-Abonnements bietet und eine Reihe cooler Funktionen bietet.

Warum wird es benötigt?

  • Erstellen Sie eine Bindung, indem Sie die Eingabe für das Wochenende abonnieren.
  • Die Möglichkeit, sich am Ende des Lebenszyklus abzumelden (wenn es sich um ein abstraktes Konzept handelt und nichts mit Android zu tun hat).
  • Bonus: Mit Binder können Sie Zwischenobjekte hinzufügen, z. B. zum Protokollieren oder Zeitreise-Debuggen.

Anstatt manuell zu signieren, können Sie die obigen Beispiele wie folgt umschreiben:

 val binder = Binder() binder.bind(wishes to feature) binder.bind(feature to logger) 

Dank Kotlin sieht alles sehr einfach aus.

Diese Beispiele funktionieren, wenn die Art der Eingabe und Ausgabe gleich ist. Aber was ist, wenn es nicht ist? Durch die Implementierung der Erweiterungsfunktion können wir die Transformation automatisch durchführen:

 val output: ObservableSource<A> = TODO() val input: Consumer<B> = TODO() val transformer: (A) -> B = TODO() binder.bind(output to input using transformer) 

Achten Sie auf die Syntax: Sie liest sich fast wie ein normaler Satz (und dies ist ein weiterer Grund, warum ich Kotlin liebe). Binder wird aber nicht nur als syntaktischer Zucker verwendet, sondern ist auch nützlich für die Lösung von Problemen mit dem Lebenszyklus.

Ordner erstellen


Das Erstellen einer Instanz sieht nirgendwo einfacher aus:

 val binder = Binder() 

In diesem Fall müssen Sie sich jedoch manuell abmelden und binder.dispose() aufrufen, wenn Sie Abonnements löschen müssen. Es gibt noch einen anderen Weg: Injizieren Sie die Lebenszyklusinstanz in den Konstruktor. So:

 val binder = Binder(lifecycle) 

Jetzt müssen Sie sich keine Gedanken mehr über Abonnements machen - sie werden am Ende des Lebenszyklus gelöscht. Gleichzeitig kann der Lebenszyklus viele Male wiederholt werden (z. B. der Start- und Stoppzyklus in der Android-Benutzeroberfläche) - und Binder erstellt und löscht jedes Mal Abonnements für Sie.

Und was ist ein Lebenszyklus?


Die meisten Android-Entwickler, die den Ausdruck "Lebenszyklus" sehen, repräsentieren die Aktivitäts- und Fragmentzyklen. Ja, Binder kann mit ihnen arbeiten und sich am Ende des Zyklus abmelden.

Dies ist jedoch nur der Anfang, da Sie die Android-Oberfläche LifecycleOwner in keiner Weise verwenden - Binder hat eine eigene, universellere. Es ist im Wesentlichen ein BEGIN / END-Signalstrom:

 interface Lifecycle : ObservableSource<Lifecycle.Event> { enum class Event { BEGIN, END } // Remainder omitted } 

Sie können diesen Stream entweder mit Observable (durch Zuordnung) implementieren oder einfach die ManualLifecycle- Klasse aus der Bibliothek für Nicht-Rx-Umgebungen verwenden (siehe genau unten).

Wie funktioniert Bindemittel ? Beim Empfang eines BEGIN-Signals werden Abonnements für die zuvor konfigurierten Komponenten ( Eingabe / Ausgabe ) erstellt, und beim Empfang eines END-Signals werden diese gelöscht. Das Interessanteste ist, dass Sie von vorne anfangen können:

 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") // will print: // 2 // 3 // 5 // 6 

Diese Flexibilität bei der Neuzuweisung von Abonnements ist besonders nützlich, wenn Sie mit Android arbeiten, wenn zusätzlich zum üblichen Erstellen-Zerstören mehrere Start-Stopp- und Wiederaufnahme-Pause-Zyklen möglich sind.

Android Binder Lebenszyklen


Es gibt drei Klassen in der Bibliothek:

  • CreateDestroyBinderLifecycle ( androidLifecycle )
  • StartStopBinderLifecycle ( androidLifecycle )
  • ResumePauseBinderLifecycl e ( androidLifecycle )

androidLifecycle ist der Wert, der von der Methode getLifecycle() wird, dh AppCompatActivity , AppCompatDialogFragment usw. Alles ist sehr einfach:

 fun createBinderForActivity(activity: AppCompatActivity) = Binder(   CreateDestroyBinderLifecycle(activity.lifecycle) ) 

Individuelle Lebenszyklen


Hören wir hier nicht auf, denn wir sind in keiner Weise an Android gebunden. Was ist der Lebenszyklus eines Bindemittels ? Im wahrsten Sinne des Wortes: Zum Beispiel die Wiedergabezeit eines Dialogfelds oder die Ausführungszeit einer asynchronen Aufgabe. Sie können es beispielsweise an den DI-Bereich binden - und dann wird jedes Abonnement damit gelöscht. Volle Handlungsfreiheit.

  1. Möchten Sie, dass Abonnements gespeichert werden, bevor das Observable den Artikel sendet? Konvertieren Sie dieses Objekt in Lifecycle und übergeben Sie es an Binder . Implementieren Sie den folgenden Code in der Erweiterungsfunktion und verwenden Sie ihn später:

     fun Observable<T>.toBinderLifecycle() = Lifecycle.wrap(this   .first()   .map { END }   .startWith(BEGIN) ) 
  2. Möchten Sie Ihre Bindungen behalten, bis Completable fertig ist? Keine Probleme - dies erfolgt analog zum vorherigen Absatz:

     fun Completable.toBinderLifecycle() = Lifecycle.wrap(   Observable.concat(       Observable.just(BEGIN),       this.andThen(Observable.just(END))   ) ) 
  3. Möchten Sie, dass ein anderer Nicht-Rx-Code entscheidet, wann Abonnements entfernt werden sollen? Verwenden Sie ManualLifecycle wie oben beschrieben.

In jedem Fall können Sie entweder einen reaktiven Stream in den Lifecycle.Event- Elementstrom legen oder ManualLifecycle verwenden, wenn Sie mit Nicht-Rx-Code arbeiten.

Allgemeine Systemübersicht


Binder verbirgt die Details zum Erstellen und Verwalten von Rx-Abonnements. Es bleibt nur eine kurze, allgemeine Übersicht: „Komponente A interagiert mit Komponente B in Bereich C“.

Angenommen, wir haben die folgenden reaktiven Komponenten für den aktuellen Bildschirm:



Wir möchten, dass die Komponenten innerhalb des aktuellen Bildschirms verbunden werden, und wir wissen, dass:

  • UIEvent kann direkt an AnalyticsTracker weitergeleitet werden .
  • UIEvent kann in Wish for Feature umgewandelt werden .
  • Der Status kann in ein ViewModel für eine Ansicht umgewandelt werden .

Dies kann in mehreren Zeilen ausgedrückt werden:

 with(binder) {   bind(feature to view using stateToViewModelTransformer)   bind(view to feature using uiEventToWishTransformer)   bind(view to analyticsTracker) } 

Wir machen solche Quetschungen, um die Verbindung von Komponenten zu demonstrieren. Und da wir Entwickler mehr Zeit damit verbringen, Code zu lesen als ihn zu schreiben, ist eine so kurze Übersicht äußerst nützlich, insbesondere wenn die Anzahl der Komponenten zunimmt.

Fazit


Wir haben gesehen, wie Binder bei der Verwaltung von Rx-Abonnements hilft und wie Sie sich einen Überblick über ein System verschaffen können, das aus reaktiven Komponenten besteht.

In den folgenden Artikeln wird beschrieben, wie wir reaktive UI-Komponenten von der Geschäftslogik trennen und wie Sie mithilfe von Binder Zwischenobjekte hinzufügen (zum Protokollieren und Debuggen von Zeitreisen). Nicht wechseln!

In der Zwischenzeit können Sie die Bibliothek auf GitHub überprüfen.

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


All Articles