
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:
- Welche Elemente sollten beim Hinzufügen neuer reaktiver Komponenten verwendet werden?
- Was ist der einfachste Weg, um Ihre Abonnements zu verwalten?
- 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.*; public interface ObservableSource<T> { 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).
public interface Consumer<T> { 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:
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 }
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")
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.
- 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) )
- 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)) ) )
- 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.