Hallo, mein Name ist Andrey und ich arbeite an den Anwendungen Tinkoff und Tinkoff Junior für die Android-Plattform. Ich möchte darüber sprechen, wie wir zwei ähnliche Anwendungen aus einer Codebasis sammeln.
— , ̆ 14 . , (, ), , , (, ).
. 

Zu Beginn des Projekts haben wir verschiedene Optionen für seine Umsetzung geprüft und eine Reihe von Entscheidungen getroffen. Es wurde sofort klar, dass die beiden Anwendungen (Tinkoff und Tinkoff Junior) einen erheblichen Teil des gemeinsamen Codes haben würden. Wir wollten uns nicht von der alten Anwendung trennen und dann die Fehlerkorrekturen und die neuen allgemeinen Funktionen kopieren. Um mit zwei Anwendungen gleichzeitig arbeiten zu können, haben wir drei Optionen in Betracht gezogen: Gradle Flavours, Git-Submodule, Gradle-Module.
Gradle Aromen
Viele unserer Entwickler haben bereits versucht, Aromen zu verwenden, und wir könnten mehrdimensionale Aromen für die Verwendung mit vorhandenen Aromen verwenden.
Aromen haben jedoch einen fatalen Fehler. Android Studio betrachtet den Code nur als den Code der aktiven Variante - das heißt, was sich im Hauptordner und im Geschmacksordner befindet. Der Rest des Codes wird zusammen mit Kommentaren als Text betrachtet. Dies führt zu Einschränkungen für einige Studio-Tools: Suche nach Code-Verwendung, Refactoring und andere.
Git-Submodule
Eine weitere Möglichkeit zur Implementierung unserer Idee besteht darin, die Submodule des Git zu verwenden: Übertragen Sie den gemeinsamen Code in ein separates Repository und verbinden Sie ihn als Submodul mit dem Code für eine bestimmte Anwendung mit zwei Repositorys.
Dieser Ansatz erhöht die Komplexität der Arbeit mit dem Quellcode des Projekts. Außerdem müssten Entwickler weiterhin mit allen drei Repositorys arbeiten, um Änderungen vorzunehmen, wenn die API des gemeinsamen Moduls geändert wird.
Multi-Modul-Architektur
Die letzte Option ist der Wechsel zur Architektur mit mehreren Modulen. Dieser Ansatz ist frei von den Nachteilen, die die beiden anderen haben. Der Übergang zu einer Architektur mit mehreren Modulen erfordert jedoch zeitaufwändiges Refactoring.
Zu Beginn unserer Arbeit an Tinkoff Junior hatten wir zwei Module: ein kleines API-Modul, das die Arbeit mit dem Server beschreibt, und ein großes monolithisches Anwendungsmodul, in dem der Großteil des Projektcodes konzentriert war.


Aus diesem Grund wollten wir zwei Anwendungsmodule erhalten: Adult und Junior sowie einige gemeinsame Kernmodule. Wir haben zwei Optionen identifiziert:
- Gemeinsamen Code in das gemeinsame Modul einfügen. Dieser Ansatz ist „korrekter“, dauert jedoch länger. Wir haben das Volumen der Wiederverwendung von Code auf ungefähr 80% geschätzt.

- Konvertieren Sie das Anwendungsmodul in eine Bibliothek und verbinden Sie diese Bibliothek mit den Thin Adult- und Junior- Modulen. Diese Option ist schneller, bringt jedoch Code zu Tinkoff Junior, der niemals ausgeführt wird.

Wir hatten Zeit in Reserve und beschlossen, die Entwicklung gemäß der ersten Option (dem gemeinsamen Modul) mit der Bedingung zu beginnen, auf die schnelle Option umzuschalten, wenn wir keine Zeit mehr für das Refactoring haben.
Am Ende geschah dies: Wir haben einen Teil des Projekts in das gemeinsame Modul übertragen und dann das verbleibende Anwendungsmodul in eine Bibliothek umgewandelt. Als Ergebnis haben wir jetzt die folgende Projektstruktur:

Wir haben Module mit Funktionen, mit denen wir zwischen einem "Erwachsenen" -, einem allgemeinen oder einem "Kinder" -Code unterscheiden können. Das Anwendungsmodul ist jedoch immer noch groß genug, und jetzt ist etwa die Hälfte des Projekts dort gespeichert.
Die Anwendung in eine Bibliothek verwandeln
Die Dokumentation enthält einfache Anweisungen zum Verwandeln einer Anwendung in eine Bibliothek. Es enthält vier einfache Punkte, und es scheint, dass keine Schwierigkeiten bestehen sollten:
- Öffnen
build.gradle
Datei build.gradle
Moduls - Entfernen Sie die
applicationId
aus der Modulkonfiguration - Ersetzen Sie am Anfang der Datei das
apply plugin: 'com.android.application'
durch apply plugin: 'com.android.application'
apply plugin: 'com.android.library'
- Speichern Sie die Änderungen und synchronisieren Sie das Projekt in Android Studio ( Datei> Projekt mit Gradle-Dateien synchronisieren).
Die Konvertierung dauerte jedoch mehrere Tage und der resultierende Unterschied stellte sich wie folgt heraus:
- 183 Dateien geändert
- 1601 Einfügungen (+)
- 1920 Streichungen (-)
Was ist schief gelaufen?
Erstens sind Ressourcenkennungen in Bibliotheken keine Konstanten . In Bibliotheken wie in Anwendungen wird eine R.java- Datei mit einer Liste von Ressourcenkennungen generiert . In Bibliotheken sind die Bezeichnerwerte nicht konstant. In Java können Sie nicht konstante Werte nicht aktivieren, und alle Schalter müssen durch if-else ersetzt werden.
Als nächstes stießen wir auf eine Paketkollision.
Angenommen, Sie haben eine Bibliothek mit package = com.example , und die Anwendung mit package = com.example.app hängt von dieser Bibliothek ab. Dann wird die Klasse com.example.R in der Bibliothek bzw. com.example.app.R in der Anwendung generiert. Erstellen wir nun die Aktivität com.example.MainActivity in der Anwendung, in der wir versuchen, auf die R-Klasse zuzugreifen. Ohne expliziten Import wird die R-Klasse der Bibliothek verwendet, in der nicht die Anwendungsressourcen, sondern nur die Bibliotheksressourcen angegeben sind. Android Studio hebt den Fehler jedoch nicht hervor, und wenn Sie versuchen, von Code zu einer Ressource zu wechseln, ist alles in Ordnung.
Dolch
Wir verwenden Dolch als Rahmen für die Abhängigkeitsinjektion.
In jedem Modul, das Aktivitäten, Fragmente und Dienste enthält, haben wir die üblichen Schnittstellen, die die Inject- Methoden für diese Entitäten beschreiben. In Anwendungsmodulen ( Adult und Junior ) erben die Dolchkomponentenschnittstellen von diesen Schnittstellen. In den Modulen bringen wir die Komponenten zu den für dieses Modul notwendigen Schnittstellen.
Multibindings
Die Entwicklung unseres Projekts wird durch die Verwendung von Multibindings erheblich vereinfacht.
In einem der gängigen Module definieren wir eine Schnittstelle. In jedem Anwendungsmodul ( Erwachsener , Junior ) beschreiben wir die Implementierung dieser Schnittstelle. Mithilfe der Annotation @Binds
wir dem Dolch mit, dass jedes Mal anstelle einer Schnittstelle die spezifische Implementierung für eine Kinder- oder Erwachsenenanwendung eingefügt werden muss. Wir sammeln auch häufig eine Sammlung von Schnittstellenimplementierungen (Set oder Map), und solche Implementierungen werden in verschiedenen Anwendungsmodulen beschrieben.
Aromen
Für verschiedene Zwecke sammeln wir verschiedene Anwendungsoptionen. Die im Basismodul beschriebenen Geschmacksrichtungen müssen auch in den abhängigen Modulen beschrieben werden. Damit Android Studio ordnungsgemäß funktioniert, müssen in allen Projektmodulen kompatible Baugruppenoptionen ausgewählt werden.
Schlussfolgerungen
In kurzer Zeit haben wir eine neue Anwendung implementiert. Jetzt versenden wir die neue Funktionalität in zwei Anwendungen und schreiben sie einmal.
Gleichzeitig haben wir einige Zeit mit Refactoring verbracht, gleichzeitig die technische Verschuldung reduziert und auf eine Architektur mit mehreren Modulen umgestellt. Auf dem Weg stießen wir auf Einschränkungen durch das Android SDK und Android Studio, die wir erfolgreich verwaltet haben.