Hallo, mein Name ist Alexei Valyakin, ich schreibe Anwendungen für Android. Vor einigen Monaten sprach ich bei einem Treffen des Yandex.Taxi-Teams mit mobilen Entwicklern. Mein Bericht war dem Übergang zur Architektur von RIBs in Taxis gewidmet (RIB steht für die drei Top-Router, Interactor, Builder). Hier ist das Video und unter dem Schnitt - ein Kompendium:
- Es ist Zeit, mit einem Hype ein wenig in einen Zug zu springen. Dies ist ein klassisches Thema über Architektur in Android.
Im Ernst, heute möchte ich Ihnen erzählen, wie und warum wir diese Architektur implementiert haben, auf welche Schwierigkeiten wir gestoßen sind und wie sie Ihnen helfen kann.

Als ich in die Firma eintrat, bestand unser Team aus vier Personen. Bereits in diesem Moment hatten wir eine Vielzahl von Schwierigkeiten. Das Projekt war alt, es begann im Jahr 2012. Es wurden einige technische Probleme gesammelt, von denen eines ein falsch aufgebautes CI ist, eine große Variabilität der Ansätze und Tests, die nicht alles abdecken. Und im Allgemeinen gab es eine große Anzahl von Schwierigkeiten und Zusammenführungskonflikten.
In zwei Jahren sind wir auf 12 Personen angewachsen, was bedeutet, dass wir die Parallelisierung der Entwicklung von Funktionen verstärkt haben. Infolgedessen gibt es noch mehr Zusammenführungskonflikte, und mit viel Kohärenz des Codes verstehen Sie, wozu dies führen kann. Irgendwann fingen wir gerade an zu sinken und irgendwie mussten wir es herausfinden. Ein Teil dieser Probleme wurde durch Punktumgestaltung gelöst, ein Teil durch die Komponentenbibliothek, über die in einem separaten Bericht gesprochen werden sollte.

Was wollen alle Entwickler? Wunderschöne Architektur, Entwicklungsflexibilität, einfaches Hinzufügen von Funktionen und natürlich Reduzierung der Komplexität von Zusammenführungen - da sie im Grunde genommen einige Fehler verursachen, die in der Release-Phase auftreten können, wenn isolierte Funktionen getestet werden und gut funktionieren. Und als sie den Release-Hop ergriffen und erreichten, fiel alles auseinander. Dies ist ein Beispielbild, zu dem wir kommen wollten.

Wie kannst du zu ihr gehen? Es ist klar, dass es viele Möglichkeiten gibt, etwas gut zu machen. Ich werde über die wichtigsten Ansätze und ihre Nachteile sprechen. Natürlich gibt es auch andere Lösungen.

Klassisches MVP. Vor welchen Herausforderungen stehen wir im klassischen MVP? Wenn wir uns das Beispiel unseres Projekts ansehen, stellen wir fest, dass es MVP-Aktivität, MVP-Fragment und MVP-Ansicht gibt. Und es stellt sich heraus, dass das, was hinzugefügt werden muss, sehr unterschiedlich ist. In einigen Fällen denken Sie, Sie müssen eine Ansicht und ein Fragment hinzufügen. Dann stellt sich heraus, dass das Hinzufügen einer kleinen Funktion, mit der der Manager zu Ihnen kommt, ziemlich schwierig ist, da sie sich im Allgemeinen in einer separaten MVP-Aktivität befindet.
Das zweite Problem, das MVP hat, hängt mit der Tatsache zusammen, dass der Router bittet. Sie möchten Kinder flexibel verbinden und damit eine Art Essenz dafür haben. Daher kommen MVPs normalerweise zu einem selbstgemachten Router oder etwas anderem. Und der ansichtsorientierte Ansatz ist ein ziemlich großes Minus. In sehr vielen MVP-Mustern wird der Präsentator in die Ansicht eingefügt. Dies macht sie bereits weniger passiv und verstößt gegen eine saubere Architektur.

Viper ist besser. Er hat eine solche Entität wie ein Router, er ist abstrakter und hat dennoch eine Reihe von Minuspunkten. Es verfügt weiterhin über eine ansichtsgesteuerte Logik, die erforderliche Präsentationsschicht, durch die die Geschäftslogik geleitet wird, und dies ist nicht immer der Fall. Die Ansichtsebene ist ebenfalls erforderlich. Sie können sie nicht entfernen.
Das Hauptproblem ist, dass diese Architektur aus der iOS-Welt zu uns kam und daher auf eine bestimmte Weise für Android angepasst werden muss. Ich habe gesehen, dass es einige Anpassungen gibt, und einige von ihnen sogar nichts, aber es gibt Nachteile.

Es ist klar, dass es in der Welt der Architektur keine Silberkugel gibt, jede Architektur hat ihre Vor- und Nachteile. RIBs haben auch Nachteile. Im Allgemeinen hat Uber diese Architektur größtenteils auf Konzeptebene eingeführt. Sie haben einige offene Klassen, es gibt keine komplizierten Beispiele. Es gibt einige einfache Tutorials, die Sie durchgehen können. Und wenn Sie zu einer beliebigen Architektur wechseln, folgt eine große Menge an Refactoring, was Sie tun müssen, aber nicht nur RIBs haben dieses Minus.

Woraus besteht die RIB-Architektur? Sie verwendet Dolchkomponenten. Ihre Hauptklasse, Builder, vereint diese gesamte Komponente, die aus folgenden Teilen besteht: Router, Interactor. Präsentator (Ansicht) - eine separate Ebene, manchmal vorhanden, manchmal nicht vorhanden. Gleichzeitig kann Presenter (Ansicht) zu einer Klasse zusammengeführt oder aufgeteilt werden, wenn Sie eine falsche Präsentationslogik haben.
Was ist hier noch cool? Da Presenter (Ansicht) optional sind, fügen Sie neue Bildschirme ähnlich wie neue Geschäftsfunktionen hinzu. Ihre Struktur ist sauberer und verständlicher. Das Kind weiß nichts über die Eltern, und die Eltern wissen über die Kinder Bescheid. Mal sehen, wie dies als Beispiel für eine vereinfachte Struktur aussieht.

Sie haben immer eine Art Wurzel. Dies ist die Root-RIB. Abhängig vom Status Ihrer Anwendung entscheidet es, was in sich selbst aufgenommen werden soll: Es handelt sich entweder um einen autorisierten oder einen nicht autorisierten Status. Schauen wir uns ein Beispiel unserer Anwendung an. Vielleicht sind Sie auf der Bestellung oder nicht auf der Bestellung.
Als Beispiel eine weitere coole RIB-Funktion. Sie können eine RIB als modalen Bildschirm erstellen und dann grundsätzlich von jeder RIB aus verbinden. Da die RIB nichts über Eltern weiß, kann jeder Elternteil die Abhängigkeiten bereitstellen, die für die untergeordnete RIB erforderlich sind.

Die Struktur der Module könnte ungefähr so aussehen. Im Moment haben wir nur darüber nachgedacht, unsere Anwendung in Module aufzuteilen. Er war allein mit uns. In der Tat ist alles ganz klassisch implementiert. Sie haben eine Art Common-Modul, das je nach Bedarf in noch kleinere Module unterteilt werden kann. Sie haben eine Art Kern-API, vielleicht ein Netzwerk, Datenbanken usw. Und in unserem Koordinatensystem ist eine bestimmte RIB ein separates Modul, das alle allgemeinen usw. enthält, was es benötigt, einschließlich Kinder-RIBs.
Wenn einige Dinge zwischen mehreren RIBs kombiniert werden müssen, gibt es Beispiele mit gemeinsam genutzten Feature-Classes, die sich einfach in separaten Modulen abheben.

Was sind die Vorteile von RIBs? Einfache Tests, hohe Code-Isolation, Ansatz mit nur einer Aktivität, keine Schmerzen mit Fragmenten (wer auch immer arbeitet, wird es verstehen) und Einheitlichkeit. Dies ist eine plattformübergreifende Architektur. Es gibt einen Ansatz für iOS und Android. Und wenn Sie zwei Teams haben, ist dies ein großes Plus, da sie dieselbe Sprache sprechen.
Dies ist ein wichtiger Punkt. Willst du einen kleinen Life-Hack über die Implementierung von RIBs? Angenommen, Sie übertragen Abhängigkeiten auf sich selbst, dann fügen Sie die Erweiterungsfunktionen der Erben hinzu und verstehen, dass Ihnen all dies nicht ausreicht. Sie müssen es für sich selbst anpassen. Am Ende nehmen Sie sie einfach und übertragen sie in Ihre Klassen. Und es gibt noch einen anderen Weg: Wenn Sie sie sofort in Ihre Klassen übertragen, ohne Zeit mit der ersten Option zu verschwenden, und sie für sich selbst anpassen.
Es ist Zeit, einen Blick auf den Code zu werfen und wie alles aussieht.

Sie verfügen über ein praktisches Plug-In, mit dem Sie die für RIB erforderlichen Klassen generieren können, ohne Zeit mit deren Erstellung zu verschwenden. Er erstellt vier Hauptklassen - Builder, Interactor, Router und View, über die ich auf den folgenden Folien ausführlicher sprechen werde. Es werden auch Tests generiert. Natürlich wird er sie nicht für Sie schreiben, und Sie müssen sie selbst schreiben, aber es ist trotzdem ganz nett. Jetzt denken wir darüber nach, ein Plugin zu erstellen, das die Erstellung neuer Module mit RIBs vereinfacht. Dieses Plugin würde sofort alle erforderlichen Abhängigkeiten verbinden und die Konfiguration des Moduls würde weniger Zeit in Anspruch nehmen.

Builder ist also eine klassische Klebercodekomponente, deren Hauptaufgabe darin besteht, alles zusammenzusetzen, eine Dolchkomponente und eine Ansicht zusammenzusetzen. Normalerweise ruft View nur den Konstruktor auf, da ist nichts kompliziert. In einigen Fällen kann es aufgeblasen sein.

Der zweite Teil, der sich in Builder befindet, befasst sich mit Sucht, dh wie das Kind Sucht von außen bekommt.

Es verfügt über eine übergeordnete Komponentenschnittstelle, die die benötigten Abhängigkeiten definiert. Somit werden im Builder der untergeordneten Komponente alle Abhängigkeiten bereitgestellt, die von oben benötigt werden.

Interactor ist im Wesentlichen die wichtigste Klasse, die Geschäftslogik ist. Es sind nur Injektionen erlaubt. Dies ist praktisch das Wichtigste, was getestet wird. Es empfängt ein Ereignis von der UI-Ebene mithilfe von Stream RX-Ereignissen. Presenter ist eine Schnittstelle, die die von meinem Ereignis bereitgestellten Methoden definiert.
Was ist für RIBs sonst noch praktisch? Durch die Tatsache, dass Sie auf der Ebene Interactor und Presenter die Interaktion organisieren können, die Ihnen gefällt. Dies können MVP, MVVM und MVVI sein. Hier kann jeder frei wählen, was er mag. Ein Abonnement für Presenter-Ereignisse könnte ungefähr so aussehen.

Und so könnte die Verarbeitung dieser Ereignisse aussehen.

Router - eine Klasse, die für die Verbindung von Kindern verantwortlich ist. Er hat keine Geschäftslogik, er selbst bringt Kinder nicht dazu, sich zu verbinden. Dies macht Interactor zu einem solchen Konzept. Im Wesentlichen gebe ich hier ein vereinfachtes Beispiel dafür, wie dies geschieht. Tatsächlich ruft Builder einfach die Build-Methode auf, die die untergeordnete RIB sammelt und das untergeordnete Element direkt über das angehängte untergeordnete Element verbindet und eine Ansicht hinzufügt. Meistens kann diese Logik in einem separaten Übergang gekapselt werden. Sie können Animationen konfigurieren - alles hängt von Ihren Anforderungen ab.

Die Ansicht ist in dieser Architektur so passiv wie möglich. Sie spritzt sich nichts ein, sie weiß fast nichts. In den einfachsten Fällen kann die Presenter-Oberfläche implementiert werden, wenn Sie keine Schwierigkeiten haben, sie zu präsentieren. In komplexeren Fällen wird diese Logik in zwei Klassen unterteilt. Das heißt, Sie haben einen separaten Klassen-Presenter, der nur Geschäftsdaten abbildet - beispielsweise in der Modellansicht.
Hier ist ein Beispiel dafür, wie Interactor UI-Ereignisse empfängt. Schauen Sie sich den Rx-Stream an.

Sie können nicht einfach eine neue Architektur erstellen. Wenn Sie dies tun, insbesondere bei einem großen Projekt, treten bestimmte Schwierigkeiten auf. Sie müssen verstehen, dass wir ein riesiges Projekt haben: ungefähr 20 Aktivitäten, wenn nicht mehr, und ungefähr 60 Fragmente. All diese Logik war sehr fragmentiert. Es war notwendig, das alles irgendwie zusammenzuführen.

Zunächst müssen Sie alles zu einem einzigen Navigationspunkt zusammenführen und zunächst ein Gottobjekt erstellen - einen Aktivitätsrouter, in dem Sie auch einen Stapel von Fragmenten verwalten, da Sie viel alten Code haben. Niemand lässt Sie den ganzen Tag eine neue Architektur implementieren und ein Unternehmen stoppen. Dabei müssen Sie sich mit einem Stapel RIBs anfreunden. RIBs haben natürlich auch einen Stapel - er ist unter der Haube zugänglich. Aber was ist hier wichtig? Sehr viel Code muss von uns selbst ausgefüllt werden. Uber unterstützt keine Bildschirmrotation, daher geht es nicht so sehr um die Wiederherstellung des Status. Als ich anfing, diese Architektur zu studieren, musste ich als erstes einen Erben über den Router hinzufügen, der die Wiederherstellung der Hierarchie der RIBs und des gesamten Status der Anwendung unterstützt.
Sie müssen das Umschalten von Funktionen unterstützen. Kein einziges großes Projekt kann darauf verzichten. Jetzt entwickelt einer unserer Entwickler ein Konzept. Wenn jemand Mobius 2016 gesehen hat, haben wir über Plugin Factory
gesprochen , mit dem Sie bestimmte Logikblöcke dynamisch verbinden und trennen können - nicht unbedingt Teile mit Bildschirmen. Dies kann beispielsweise abhängig von den Experimenten geschehen, die vom Server stammen. Alles wird abstrakt gemacht und die Interaktion wird vereinfacht.
Der RIB-Workflow ist auch ein interessantes Konzept, das Sie möglicherweise benötigen. Dies ist der Fall, wenn Sie mehrere RIBs haben, die nichts voneinander wissen, sich ungefähr auf derselben Ebene befinden, aber gleichzeitig den Prozess mit Daten an der Eingabe starten müssen und am Ende alles zusammenfügen müssen.
Und zum Beispiel modale Bildschirme. Wir haben ein super-benutzerdefiniertes Design, so dass fast keine klassischen Dialoge mehr vorhanden sind. Alles ist selbst geschrieben, wir müssen alles selbst implementieren.
Was können Sie mit RIBs bekommen? Isolierung von Code, verständliche einfache Architektur, einfache Möglichkeit zur Modularisierung, Beseitigung von Fragmenten, Ansatz mit einer Aktivität und Bequemlichkeit der parallelen Entwicklung von Funktionen.
Referenzen:
-
github.com/uber/RIBs-
github.com/uber/RIBs/tree/master/android/tutorials-
habr.com/de/company/livetyping/blog/320452-
youtu.be/Q5cTT0M0YXg-
github.com/xzaleksey/Role-Playing-System-V2-
github.com/xzaleksey/DeezerSampleDer letzte Link führt zu meinem Lieblingsprojekt. Vor sechs Monaten begann ich mit der Entwicklung von RIBs, um diese Architektur auszuprobieren, bevor ich sie uns vorstellte. Und es gibt mehr oder weniger reale Fälle, die Ihnen helfen können. Ich habe viel experimentiert, daher gibt es kontroverse Dinge. Aber im Allgemeinen können Sie sehen, wie es dort funktioniert. Und vielleicht nimmst du von dort etwas für dich.
Wir denken auch darüber nach, all dies einer separaten Bibliothek zuzuweisen, wie wir es in unserer Zeit mit der Komponentenbibliothek getan haben. Es wird solche Yandex-Rippen geben. Aber das ist in der Zukunft. Ich habe dir alles erzählt, danke.