Moderne MVot-basierte Kotlin-Architektur

Ursprünglicher Autor: Zsolt Kocsi
  • Übersetzung


In den letzten zwei Jahren haben Android-Entwickler bei Badoo einen langen und schwierigen Weg von MVP zu einem völlig anderen Ansatz in der Anwendungsarchitektur eingeschlagen. ANublo und ich möchten eine Übersetzung des Artikels unseres Kollegen Zsolt Kocsi veröffentlichen , in der die Probleme und deren Lösung beschrieben werden.

Dies ist der erste von mehreren Artikeln, die sich mit der Entwicklung moderner MVI-Architektur bei Kotlin befassen.

Beginnen wir am Anfang: Zustandsprobleme


Zu jedem Zeitpunkt hat eine Anwendung einen bestimmten Status, der ihr Verhalten und das, was der Benutzer sieht, bestimmt. Wenn Sie sich nur auf ein Klassenpaar konzentrieren, umfasst dieser Status alle Werte von Variablen - von einfachen Flags bis hin zu einzelnen Objekten. Jede dieser Variablen lebt ein eigenes Leben und wird von verschiedenen Teilen des Codes gesteuert. Sie können den aktuellen Status der Anwendung nur ermitteln, indem Sie sie alle einzeln überprüfen.

An dem Code erstellen wir ein bestehendes Systemmodell in unserem Kopf. Wir realisieren leicht die Idealfälle, wenn alles nach Plan verläuft, aber nicht alle möglichen Probleme und Bedingungen der Anwendung berechnet werden können. Früher oder später wird uns einer der Zustände, die wir nicht vorausgesehen haben, überholen, und wir werden auf einen Fehler stoßen.

Zunächst wird der Code gemäß unseren Vorstellungen darüber geschrieben, wie das System funktionieren soll. Später müssen Sie jedoch fünf Phasen des Debugging durchlaufen , und Sie müssen alles schmerzhaft wiederholen und gleichzeitig das vorhandene Modell in Ihrem Kopf ändern. Es bleibt zu hoffen, dass früher oder später ein Verständnis für das, was schiefgegangen ist, zu uns kommt und der Fehler behoben wird.

Glück ist aber nicht immer. Je komplexer das System ist, desto wahrscheinlicher ist es, dass es auf unvorhergesehene Bedingungen stößt, deren Debugging eine lange Nacht in Albträumen dauern wird.

In Badoo sind alle Anwendungen im Wesentlichen asynchron - nicht nur wegen der umfangreichen Funktionalität, die dem Benutzer über die Benutzeroberfläche zur Verfügung steht, sondern auch wegen der Möglichkeit, Daten in eine Richtung vom Server zu senden. Der Status und das Verhalten der Anwendung wird erheblich beeinflusst - von der Änderung des Zahlungsstatus bis hin zu neuen Übereinstimmungen und Überprüfungsanfragen.

In unserem Chat-Modul sind wir auf einige seltsame und schwer reproduzierbare Fehler gestoßen, die viel Blut verdorben haben. Manchmal gelang es Testern, sie aufzuschreiben, sie wiederholten sich jedoch nicht auf dem Entwicklergerät. Aufgrund des asynchronen Codes war eine vollständige Wiederholung der einen oder anderen Ereigniskette äußerst unwahrscheinlich. Und da die Anwendung nicht herunterfiel, hatten wir nicht einmal eine Stack-Trace, die zeigte, wo die Suche beginnen sollte.

Saubere Architektur ( reine Architektur) konnte uns auch nicht helfen. Selbst nachdem wir das Chat-Modul neu geschrieben hatten, zeigten die A / B-Tests kleine, aber erhebliche Inkonsistenzen bei der Anzahl der Nachrichten von Benutzern, die die neuen und alten Module verwendet haben. Wir entschieden, dass dies auf die schwer reproduzierbare Fehler und den Zustand des Rennens zurückzuführen ist. Die Diskrepanz hielt an, nachdem alle anderen Faktoren geprüft wurden. Die Interessen des Unternehmens litten, die Entwickler hatten Schwierigkeiten, den Code aufrecht zu erhalten.

Es ist nicht möglich, eine neue Komponente zu veröffentlichen, wenn sie schlechter arbeitet als die vorhandene, aber es ist auch unmöglich, sie nicht zu veröffentlichen - da ein Update erforderlich war, bedeutet dies, dass es einen Grund gab. Es ist also notwendig herauszufinden, warum in einem System, das vollkommen normal aussieht und nicht abstürzt, die Anzahl der Meldungen sinkt.

Wo soll die Suche beginnen?

Spoiler: Dies ist nicht die Schuld von Clean Architecture - der menschliche Faktor ist wie immer schuld. Am Ende haben wir natürlich diese Fehler behoben, aber dafür viel Zeit und Mühe aufgewendet. Dann dachten wir uns: Gibt es einen einfacheren Weg, um diese Probleme zu vermeiden?

Das Licht am Ende des Tunnels ...


Modische Begriffe wie Model-View-Intent und „unidirektionaler Datenfluss“ sind uns bekannt. Wenn dies in Ihrem Fall nicht der Fall ist, rate ich ihnen zu google - zu diesen Themen gibt es viele Artikel im Internet. Android-Entwickler empfehlen das Hannes Dorfman-Material in acht Teilen .

Wir haben bereits Anfang 2017 mit diesen Ideen aus der Webentwicklung gespielt. Ansätze wie Flux und Redux haben sich als sehr nützlich erwiesen - sie haben uns bei vielen Problemen geholfen.

Zunächst ist es sehr nützlich, alle Statuselemente (Variablen, die sich auf die Benutzeroberfläche auswirken und verschiedene Aktionen auslösen) in einem Objekt zusammenzufassen - State. Wenn alles an einem Ort gespeichert ist, ist das Gesamtbild besser sichtbar. Wenn Sie zum Beispiel das Laden von Daten mit dieser Methode übermitteln möchten, benötigen Sie die Felder payload und isLoading . Wenn Sie sie betrachten, sehen Sie, wann die Daten empfangen werden ( Payload ) und ob die Animation dem Benutzer angezeigt wird ( isLoading ).

Wenn wir uns ferner von der parallelen Ausführung des Codes mit Rückrufen entfernen und die Statusänderungen der Anwendung in Form einer Reihe von Transaktionen ausdrücken, erhalten wir einen einzigen Einstiegspunkt. Wir stellen Ihnen den Reducer vor , der aus der funktionalen Programmierung zu uns gekommen ist. Es nimmt den aktuellen Status und die Daten für weitere Aktionen ( Intent ) und erstellt daraus einen neuen Status:

Reducer = (State, Intent) -> State

Um mit dem vorherigen Beispiel des Ladens von Daten fortzufahren, werden folgende Aktionen ausgeführt:

  • Begonnenes Laden
  • FinishedWithSuccess


Dann können Sie einen Reduzierer mit den folgenden Regeln erstellen:

  1. Erstellen Sie im Fall von StartedLoading ein neues State- Objekt, indem Sie das alte Objekt kopieren und isLoading auf true setzen.
  2. Erstellen Sie im Fall von FinishedWithSuccess ein neues State- Objekt, indem Sie das alte Objekt kopieren, wobei der Wert für isLoading auf false gesetzt wird und der Wert für die Nutzdaten
    dem geladenen Wert entspricht.

Wenn wir die resultierende State- Serie in ein Protokoll schreiben, sehen wir Folgendes:

  1. State ( payload = null, isLoading = false) - Ausgangszustand.
  2. State ( payload = null, isLoading = true) - nach dem StartedLoading.
  3. State ( payload = data, isLoading = false) - nach FinishedWithSuccess.

Wenn Sie diese Zustände mit der Benutzeroberfläche verbinden, sehen Sie alle Phasen des Prozesses: zuerst einen leeren Bildschirm, dann den Startbildschirm und schließlich die erforderlichen Daten.

Dieser Ansatz hat viele Vorteile.

  • Erstens lassen wir den Status der Rasse und die vielen unmerklichen lästigen Fehler nicht zu, indem wir den Status zentral mit einer Reihe von Transaktionen ändern.
  • Zweitens, nachdem wir eine Reihe von Transaktionen untersucht haben, können wir verstehen, was passiert ist, warum es passiert ist und wie es den Status der Anwendung beeinflusst hat. Darüber hinaus ist es mit Reducer wesentlich einfacher, alle Statusänderungen bereits vor dem ersten Start der Anwendung auf dem Gerät anzuzeigen.
  • Schließlich haben wir die Möglichkeit, eine einfache Schnittstelle zu erstellen. Da alle Zustände an einem Ort gespeichert werden (Speicher), wobei Absichten (Absichten) berücksichtigt werden, Änderungen mit Hilfe von Reducer vorgenommen werden und eine Zustandskette eindeutig dargestellt wird, bedeutet dies, dass Sie die gesamte Geschäftslogik in den Speicher aufnehmen und die Schnittstelle zum Starten von Absichten und zum Ableiten von Zuständen verwenden können.


Oder nicht

... vielleicht ein Zug, der dich anstößt


Ein Reduzierer reicht nicht aus. Wie gehe ich mit asynchronen Aufgaben mit unterschiedlichen Ergebnissen um? Wie reagiere ich auf den Push vom Server? Wie gehen Sie mit dem Start zusätzlicher Aufgaben (z. B. Löschen des Cache oder Laden von Daten aus der lokalen Datenbank) nach einer Statusänderung vor? Es stellt sich heraus, dass wir entweder nicht all diese Logik in den Reducer aufnehmen (dh eine gute Hälfte der Geschäftslogik wird nicht abgedeckt, und diejenigen, die sich für die Verwendung unserer Komponente entscheiden, müssen sich darum kümmern) oder den Reducer zwingen, alles auf einmal zu tun.

Voraussetzungen für das MVI-Framework


Natürlich möchten wir die gesamte Geschäftslogik eines separaten Features in eine separate Komponente einbinden, mit der Entwickler anderer Teams problemlos arbeiten können, indem sie einfach ihre Kopie erstellen und ihren Status abonnieren.

Außerdem:

  • es sollte leicht mit anderen Komponenten des Systems interagieren können;
  • es sollte eine klare Aufteilung der Zuständigkeiten innerhalb seiner internen Struktur geben;
  • Alle internen Teile der Komponente müssen vollständig deterministisch sein.
  • Die grundlegende Implementierung einer solchen Komponente sollte nur dann einfach und kompliziert sein, wenn zusätzliche Elemente angeschlossen werden müssen.

Wir haben nicht sofort von Reducer auf die heute verwendete Lösung umgestellt. Jedes Team hatte Probleme, wenn es unterschiedliche Ansätze gab, und eine universelle Lösung zu entwickeln, die für jeden geeignet war, schien unwahrscheinlich.

Und doch ist der aktuelle Stand für jeden geeignet. Wir freuen uns, Ihnen MVICore zu präsentieren! Der Quellcode der Bibliothek ist geöffnet und auf GitHub verfügbar .

Was macht MVICore gut?


  • Einfache Implementierung von Geschäftsfunktionen im Stil reaktiver Programmierung mit unidirektionalem Datenfluss.
  • Skalierung: Die Basisimplementierung enthält nur einen Reduzierer. In komplexeren Fällen können Sie zusätzliche Komponenten verwenden.
  • Lösung für das Arbeiten mit Ereignissen, die Sie nicht in den Status aufnehmen möchten ( Problem SingleLiveEvent ).
  • Eine einfache API, um Funktionen (und andere reaktive Komponenten Ihres Systems) an die Benutzeroberfläche und untereinander zu binden, wobei der Android-Lebenszyklus (und nicht nur) unterstützt wird.
  • Middleware-Support (dazu unten) für jede Komponente des Systems.
  • Ready Logger und die Möglichkeit der Zeitreise für jede Komponente.


Eine kurze Einführung in das Feature


Da Schritt-für-Schritt-Anleitungen bereits auf GitHub veröffentlicht wurden, werde ich auf detaillierte Beispiele verzichten und mich auf die Hauptkomponenten des Frameworks konzentrieren.

Feature ist das zentrale Element des Frameworks, das die gesamte Geschäftslogik der Komponente enthält. Feature wird durch drei Parameter definiert: Schnittstelle Feature <Wish, State, News>

Wish entspricht Intent von Model-View-Intent - dies sind die Änderungen, die wir im Modell sehen möchten (da der Begriff Intent in der Android-Entwicklerumgebung seine Bedeutung hat, mussten wir herausfinden ein anderer Name). Wunsch ist der Einstiegspunkt für Feature.

Zustand- Dies ist, wie Sie bereits verstanden haben, der Zustand der Komponente. Der Staat ist unveränderlich: Wir können seine inneren Werte nicht ändern, aber wir können neue Staaten schaffen. Dies ist die Ausgabe: Jedes Mal, wenn wir einen neuen Status erstellen, übertragen wir ihn in den Rx-Stream.

Nachrichten - eine Komponente zur Verarbeitung von Signalen, die sich nicht im Staat befinden sollten; News werden einmalig bei der Erstellung verwendet ( Problem SingleLiveEvent ). Die Verwendung von News ist optional (Sie können Nothing from Kotlin in der Feature-Signatur verwenden).

Auch im Feature muss Reducer vorhanden sein .

Feature kann die folgenden Komponenten enthalten:

  • Akteur - Führt asynchrone Tasks und / oder bedingte Statusänderungen basierend auf dem aktuellen Status durch (z. B. Formularüberprüfung). Actor bindet Wish an eine bestimmte Anzahl von Effekten und gibt sie dann an den Reducer weiter (wenn Actor Reducer den Wish direkt erhält).
  • NewsPublisher - aufgerufen, wenn Wish zu einem Effekt wird, der ein Ergebnis in Form eines neuen Staates ergibt. Aufgrund dieser Daten entscheidet er, ob er News erstellt.
  • PostProcessor wird auch aufgerufen, nachdem ein neuer Staat erstellt wurde, und er weiß auch, welcher Effekt zu seiner Erstellung geführt hat. Es startet einige zusätzliche Aktionen (Aktionen). Aktion ist "interne Wünsche" (zum Beispiel das Löschen des Caches), die nicht von außen gestartet werden können. Sie werden in Actor ausgeführt, was zu einer neuen Kette von Effekten und Staaten führt.
  • Bootstrapper ist eine Komponente, die eigenständig Aktionen ausführen kann. Seine Hauptfunktion besteht darin, das Feature zu initialisieren und / oder externe Quellen mit der Aktion zu korrelieren. Diese externen Quellen können Nachrichten von einem anderen Feature oder Serverdaten sein, die den Status ohne Benutzerinteraktion ändern müssen.


Das Schema kann einfach aussehen:


oder alle oben aufgeführten zusätzlichen Komponenten enthalten:


Das Feature selbst, das die gesamte Geschäftslogik enthält und einsatzbereit ist, sieht einfacher aus als je zuvor:



Was sonst?


Feature, der Grundstein des Gerüsts, arbeitet auf konzeptioneller Ebene. Aber die Bibliothek hat noch viel mehr zu bieten.

  • Da alle Feature-Komponenten deterministisch sind (mit Ausnahme von Actor, das nicht vollständig deterministisch ist, da es mit externen Datenquellen interagiert, der Zweig jedoch von den Eingabedaten und nicht von externen Bedingungen bestimmt wird), können alle Komponenten in Middleware eingeschlossen werden. Gleichzeitig enthält die Bibliothek bereits vorgefertigte Lösungen für die Protokollierung und Zeitreise-Debugging .
  • Die Middleware ist nicht nur auf Feature anwendbar, sondern auch auf alle anderen Objekte, die die Consumer <T> -Schnittstelle implementieren. Dies macht sie zu einem unverzichtbaren Werkzeug für das Debugging.
  • Wenn Sie einen Debugger zum Debuggen verwenden, wenn Sie sich in die entgegengesetzte Richtung bewegen, können Sie das DebugDrawer- Modul implementieren .
  • Die Bibliothek enthält ein IDEA-Plugin, mit dem Vorlagen für die häufigsten Implementierungen von Feature hinzugefügt werden können, was viel Zeit spart.
  • Es gibt Hilfsklassen, die Android unterstützen, aber die Bibliothek selbst ist nicht an Android gebunden.
  • Es gibt eine fertige Lösung für die Verknüpfung von Komponenten mit der Benutzeroberfläche und untereinander über eine grundlegende API (wir werden im nächsten Artikel darüber sprechen).

Wir hoffen, Sie probieren unsere Bibliothek aus, und ihre Verwendung wird Ihnen ebenso viel Freude bereiten wie wir - ihre Erstellung!

Am 24. und 25. November können Sie Ihr Glück versuchen und sich uns anschließen! Wir veranstalten ein mobiles Mietereignis: An einem Tag können Sie alle Stufen der Auswahl durchlaufen und ein Angebot erhalten. Meine Kollegen aus iOS- und Android-Teams werden kommen, um mit Kandidaten nach Moskau zu kommunizieren. Wenn Sie aus einer anderen Stadt kommen, berechnet Badoo Ihnen die Reisekosten. Um eine Einladung zu erhalten, bestehen Sie den Qualifikationstest für den Link . Viel Glück!

Jetzt auch beliebt: