Ende Februar haben wir ein neues Format für Meetings von Android-Entwicklern von Kaspersky Mobile Talks eingeführt . Der Hauptunterschied zu normalen Besprechungen besteht darin, dass sich „erfahrene“ Entwickler anstelle von Hunderten von Zuhörern und schönen Präsentationen zu verschiedenen Themen versammelten, um nur ein Thema zu diskutieren: Wie sie Multimodularität in ihren Anwendungen implementieren, mit welchen Problemen sie konfrontiert sind und wie sie gelöst werden.

Inhalt
- Hintergrund
- Mediatoren in HeadHunter. Alexander Blinov
- Tinkoff-Domain-Module Vladimir Kokhanov, Alexander Zhukov
- Wirkungsanalyse in Avito. Evgeny Krivobokov, Mikhail Yudin
- Wie in Tinkoff reduzierten sie die Montagezeit für PR von vierzig Minuten auf vier. Vladimir Kokhanov
- Nützliche Links
Bevor wir mit dem unmittelbaren Inhalt des Meetings im Büro von Kaspersky Lab fortfahren, erinnern wir uns, woher der Mod stammt, um die Anwendung in Module zu unterteilen (im Folgenden wird das Modul als Gradle-Modul und nicht als Dolch verstanden, sofern nicht anders angegeben).
Das Thema Multimodularität beschäftigt die Android-Community seit Jahren. Eine der Grundlagen kann als Bericht von Denis Neklyudov über den letztjährigen St. Petersburg "Mobius" angesehen werden. Er schlug vor, die monolithische Anwendung, die lange Zeit kein Thin Client mehr war, in Module zu unterteilen, um die Erstellungsgeschwindigkeit zu erhöhen.
Link zum Bericht: Präsentation , Video
Dann gab es einen Bericht von Vladimir Tagakov von Yandex.Maps über das Verknüpfen von Modulen mit Dagger. Somit lösen sie das Problem, eine einzelne Komponente von Karten für die Wiederverwendung in vielen anderen Yandex-Anwendungen zuzuweisen.
Link zum Bericht: Präsentation , Video
Auch Kaspersky Lab hat sich dem Trend nicht entzogen: Im September schrieb Evgeni Matsyuk einen Artikel darüber, wie man Module mit Dagger verbindet und gleichzeitig eine Architektur mit mehreren Modulen horizontal erstellt, ohne zu vergessen, die Prinzipien der sauberen Architektur vertikal zu befolgen.
Link zum Artikel
Und im Winter Mobius gab es zwei Berichte gleichzeitig. Zuerst sprach Alexander Blinov über die Multimodularität in der HeadHunter-Anwendung mit Toothpick als DI, und direkt nach ihm sprach Artem Zinnatulin über die Schmerzen von mehr als 800 Modulen in Lyft. Sasha begann über Multimodularität zu sprechen, um die Architektur der Anwendung zu verbessern und nicht nur die Montage zu beschleunigen.
Blinov-Bericht: Präsentation , Video
Zinnatulin-Bericht: Video
Warum habe ich den Artikel mit einer Retrospektive begonnen? Erstens hilft es Ihnen, das Thema besser zu studieren, wenn Sie zum ersten Mal über Multimodularität lesen. Und zweitens begann die erste Rede bei unserem Treffen mit einer Mini-Präsentation von Alexey Kalaida von der Firma Stream, die zeigte, wie sie ihre Anwendung auf der Grundlage von Zhenyas Artikel in Module aufteilten (und einige Punkte schienen mir Vladimirs Ansatz ähnlich zu sein).
Das Hauptmerkmal dieses Ansatzes war die Bindung an die Benutzeroberfläche: Jedes Modul ist als separater Bildschirm verbunden - ein Fragment, auf das Abhängigkeiten vom Haupt-App-Modul einschließlich des FragmentManager übertragen werden. Zunächst versuchten Kollegen, Multimodularität durch Proxy-Injektoren zu implementieren, was Zhenya in dem Artikel vorschlug. Dieser Ansatz schien jedoch überwältigend: Es gab Probleme, wenn ein Feature von einem anderen abhing, was wiederum vom dritten abhing - wir mussten für jedes Feature-Modul einen Proxy-Injektor schreiben. Der auf UI-Komponenten basierende Ansatz ermöglicht es Ihnen, keine Injektoren zu schreiben, wodurch Abhängigkeiten auf der Abhängigkeitsebene von Zielfragmenten zugelassen werden.
Die Hauptbeschränkungen dieser Implementierung: Ein Feature muss ein Fragment (oder eine andere Ansicht) sein. das Vorhandensein verschachtelter Fragmente, was zu einer großen Kesselplatte führt. Wenn ein Feature andere Features implementiert, sollte es der Abhängigkeitskarte hinzugefügt werden, die Dagger beim Kompilieren überprüft. Wenn es viele solcher Merkmale gibt, treten zum Zeitpunkt der Verknüpfung des Abhängigkeitsgraphen Schwierigkeiten auf.
Nach Alexeys Bericht ergriff Alexander Blinov das Wort. Seiner Meinung nach wäre eine an die Benutzeroberfläche gebundene Implementierung für DI-Container in Flutter geeignet. Dann wechselte die Diskussion zu einer Diskussion mit mehreren Modulen in HeadHunter. Der Zweck ihrer Unterteilung in Module war die Möglichkeit der architektonischen Isolierung von Merkmalen und der Erhöhung der Montagegeschwindigkeit.
Vor der Aufteilung in Module ist eine Vorbereitung wichtig. Zunächst können Sie ein Abhängigkeitsdiagramm erstellen - beispielsweise mit einem solchen Tool . Dies wird dazu beitragen, Komponenten mit einer minimalen Anzahl von Abhängigkeiten zu isolieren und unnötige zu entfernen (Chop). Erst danach können die am wenigsten verbundenen Komponenten zu Modulen ausgewählt werden.
Alexander erinnerte sich an die wichtigsten Punkte, über die er bei Mobius ausführlicher sprach. Eine der komplexen Aufgaben, die die Architektur berücksichtigen muss, ist die Wiederverwendung eines Moduls an verschiedenen Stellen in der Anwendung. In dem Beispiel mit der hh-Bewerbung handelt es sich um ein Lebenslaufmodul, auf das sowohl für das Stellenlistenmodul (VacanciesList), wenn der Benutzer zu dem für diese Stelle eingereichten Lebenslauf wechselt, als auch für das Negativantwortmodul (Negotiation) zugegriffen werden sollte. Aus Gründen der Klarheit habe ich das Bild, das Sasha auf einem Flipchart dargestellt hat, neu gezeichnet.

Jedes Modul enthält zwei Hauptentitäten: Abhängigkeiten - die Abhängigkeiten, die dieses Modul benötigt, und API - die Methoden, die das Modul anderen Modulen zur Verfügung stellt. Die Kommunikation zwischen den Modulen erfolgt über Mediatoren, die im Haupt-App-Modul eine flache Struktur aufweisen. Jedes Feature hat eine Auswahl. Die Mediatoren selbst sind in einem bestimmten MediatorManager im Projekt-App-Modul enthalten. Im Code sieht es ungefähr so aus:
object MediatorManager { val chatMediator: ChatMediator by lazy { ChatMediator() } val someMediator: ... } class TechSupportMediator { fun provideComponent(): SuppportComponent { val deps = object : SuppportComponentDependencies { override fun getInternalChat{ MediatorManager.rootMediator.api.openInternalChat() } } } } class SuppportComponent(val dependencies) { val api: SupportComponentApi = ... init { SupportDI.keeper.installComponent(this) } } interface SuppportComponentDependencies { fun getSmth() fun close() { scopeHolder.destroyCoordinator < -ref count } }
Alexander versprach, in Kürze ein Plug-In zum Erstellen von Modulen in Android Studio zu veröffentlichen, mit dem das Kopieren und Einfügen in seinem Unternehmen beseitigt wird, sowie ein Beispiel für ein Konsolenprojekt mit mehreren Modulen.
Weitere Fakten zu den aktuellen Ergebnissen der Trennung von hh-Anwendungsmodulen:
- ~ 83 Funktionsmodule.
- Um einen A / B-Test durchzuführen, können Features auf Mediatorebene vollständig durch das Feature-Modul ersetzt werden.
- Die Grafik von Gradle Scan zeigt, dass nach der parallelen Kompilierung der Module ein ziemlich langwieriger Prozess zum Dexen der Anwendung stattfindet (in diesem Fall zwei: für Arbeitssuchende und Arbeitgeber):

Folgendes haben Alexander und Vladimir von Tinkoff das Wort ergriffen:
Das Schema ihrer Multi-Modul-Architektur sieht folgendermaßen aus:

Module sind in zwei Kategorien unterteilt: Funktionsmodule und Domänenmodule.
Funktionsmodule enthalten Geschäftslogik- und UI-Funktionen. Sie hängen von Domänenmodulen ab, können jedoch nicht voneinander abhängen.
Domänenmodule enthalten Code für die Arbeit mit Datenquellen, dh einige Modelle, DAO (für die Arbeit mit der Datenbank), API (für die Arbeit mit dem Netzwerk) und Repositorys (kombinieren Sie die Arbeit von API und DAO). Domänenmodule können im Gegensatz zu Feature-Modulen voneinander abhängen.
Die Verbindung zwischen Domänen- und Feature-Modulen erfolgt vollständig innerhalb von Feature-Modulen (dh in der Terminologie von hh werden Abhängigkeiten und API-Abhängigkeiten von Domain-Modulen in den Feature-Modulen, die sie verwenden, vollständig aufgelöst, ohne dass zusätzliche Entitäten wie Mediatoren verwendet werden müssen).
Es folgte eine Reihe von Fragen, die ich hier im Format „Frage-Antwort“ fast unverändert stellen werde:
- Wie erfolgt die Autorisierung? Wie zieht man es in Feature-Module?
- Funktionen bei uns hängen nicht von der Autorisierung ab, da fast alle Aktionen der Anwendung in der autorisierten Zone ausgeführt werden.
- Wie können nicht verwendete Komponenten verfolgt und gereinigt werden?
- Wir haben eine Entität wie InjectorRefCount (implementiert über WeakHashMap), die beim Löschen der letzten Aktivität (oder des letzten Fragments) mit dieser Komponente diese löscht.
- Wie misst man eine "saubere" Scan- und Erstellungszeit? Wenn die Caches eingeschaltet sind, wird ein ziemlich schmutziger Scan erhalten.
- Sie können den Gradle-Cache deaktivieren (org.gradle.caching in gradle.properties).
- Wie werden Unit-Tests von allen Modulen im Debug-Modus ausgeführt? Wenn Sie nur einen Gradle-Test ausführen, werden Tests aller Geschmacksrichtungen und BuildType abgerufen.
(Diese Frage löste die Diskussion vieler Teilnehmer des Treffens aus.)
- Sie können versuchen, testDebug auszuführen.
- Dann werden Module, für die es keine Debug-Konfiguration gibt, nicht festgezogen. Es beginnt entweder zu viel oder zu wenig.
- Sie können eine Gradle-Aufgabe schreiben, die testDebug für solche Module überschreibt, oder eine gefälschte Debug-Konfiguration im Modul build.gradle vornehmen.
- Sie können diesen Ansatz folgendermaßen implementieren:
withAndroidPlugin(project) { _, applicationExtension -> applicationExtension.testVariants.all { testVariant -> val testVariantSuffix = testVariant.testedVariant.name.capitalize() } } val task = project.tasks.register < SomeTask > ( "doSomeTask", SomeTask::class.java ) { task.dependsOn("${project.path}:taskName$testVariantSuffix") }

Die nächste improvisierte Präsentation wurde von Evgeny Krivobokov und Mikhail Yudin aus Avito gemacht.
Sie verwendeten Mindmap , um ihre Geschichte zu visualisieren.
Jetzt umfasst das Projekt des Unternehmens> 300 Module, wobei 97% der Codebasis in Kotlin geschrieben sind. Der Hauptzweck der Aufteilung in Module bestand darin, die Montage des Projekts zu beschleunigen. Die Aufteilung in Module erfolgte schrittweise, wobei die am wenigsten abhängigen Teile des Codes den Modulen zugewiesen wurden. Zu diesem Zweck wurde ein Tool zum Markieren der Abhängigkeiten von Quellcodes in der Grafik für die Auswirkungsanalyse entwickelt ( Bericht über die Auswirkungsanalyse in Avito ).
Mit diesem Tool können Sie ein Feature-Modul als endgültig markieren, sodass andere Module nicht davon abhängen können. Diese Eigenschaft wird während der Auswirkungsanalyse überprüft und enthält eine Bezeichnung für explizite Abhängigkeiten und Vereinbarungen mit den Teams, die für das Modul verantwortlich sind. Basierend auf dem erstellten Diagramm wird auch die Verteilung der Änderungen überprüft, um Komponententests für den betroffenen Code auszuführen.
Das Unternehmen verwendet ein Mono-Repository, jedoch nur für Android-Quellen. Der Code anderer Plattformen lebt separat.
Gradle wird zum Erstellen des Projekts verwendet (obwohl Kollegen bereits an einen Builder wie Buck oder Bazel denken, der für Projekte mit mehreren Modulen besser geeignet ist). Sie haben bereits Kotlin DSL ausprobiert und sind dann in Gradle-Skripten zu Groovy zurückgekehrt, da es unpraktisch ist, verschiedene Versionen von Kotlin in Gradle und im Projekt zu unterstützen - die allgemeine Logik wird in Plugins integriert.
Gradle kann Aufgaben parallelisieren, zwischenspeichern und binäre Abhängigkeiten nicht neu kompilieren, wenn sich ihr ABI nicht geändert hat, was eine schnellere Zusammenstellung eines Projekts mit mehreren Modulen gewährleistet. Für ein effizienteres Caching werden Mainfraimer und mehrere selbstgeschriebene Lösungen verwendet:
- Beim Wechsel von Zweig zu Zweig hinterlässt Git möglicherweise leere Ordner, die das Caching unterbrechen ( Gradle-Problem Nr. 2463 ). Daher werden sie manuell mit dem Git-Hook gelöscht.
- Wenn Sie die Umgebung auf den Computern der Entwickler nicht steuern, können verschiedene Versionen des Android SDK und andere Parameter das Caching beeinträchtigen. Während der Erstellung des Projekts vergleicht das Skript die Umgebungsparameter mit den erwarteten: Wenn die falschen Versionen oder Parameter installiert sind, wird die Erstellung gelöscht.
- Analytics aktiviert / deaktiviert Parameter und die Umgebung. Dies dient zur Überwachung und Unterstützung von Entwicklern.
- Build-Fehler werden auch an Analytics gesendet. Bekannte und beliebte Probleme werden auf einer speziellen Seite mit einer Lösung eingetragen.
All dies trägt dazu bei, 15% Cache-Fehler bei CI und 60-80% lokal zu erzielen.
Die folgenden Gradle-Tipps können auch nützlich sein, wenn in Ihrem Projekt eine große Anzahl von Modulen angezeigt wird:
- Das Deaktivieren von Modulen über IDE-Flags ist unpraktisch. Diese Flags können zurückgesetzt werden. Daher werden Module über settings.gradle deaktiviert.
- In Studio 3.3.1 gibt es ein Kontrollkästchen "Quellengenerierung bei Gradle-Synchronisierung überspringen, wenn ein Projekt mehr als 1 Module enthält". Standardmäßig ist es ausgeschaltet, es ist besser, es einzuschalten.
- Abhängigkeiten werden in buildSrc registriert, um in allen Modulen wiederverwendet zu werden. Eine andere Option ist Plugins DSL , aber dann können Sie die Anwendung des Plugins nicht in einer separaten Datei ablegen.
Unser Treffen endete mit Vladimir von Tinkoff mit dem Clickbait-Titel des Berichts „Wie man die PR- Versammlung von 40 Minuten auf vier Minuten reduziert“. Tatsächlich sprachen wir über die Verteilung der Starts von Gradle-Plugs: Apk-Builds, Tests und statische Analysatoren.
Zunächst führten die Mitarbeiter bei jeder Pull-Anfrage eine statische Analyse durch, direkt die Montage und die Tests. Dieser Vorgang dauerte 40 Minuten, von denen nur Lint und SonarQube 25 Minuten dauerten und nur 7% der Starts fielen.
Daher wurde beschlossen, den Start in einen separaten Job zu verschieben, der alle zwei Stunden nach einem Zeitplan ausgeführt wird und im Fehlerfall eine Nachricht an Slack sendet.
Die umgekehrte Situation war die Verwendung von Detect. Es stürzte fast ständig ab, weshalb es einem vorläufigen Pre-Push-Check unterzogen wurde.
Daher blieben nur die apk-Montage- und Komponententests bei der Überprüfung der Pull-Anforderung übrig. Tests kompilieren die Quellen vor dem Ausführen, sammeln jedoch keine Ressourcen. Da die Zusammenführung von Ressourcen fast immer erfolgreich war, wurde auch die apk-Versammlung selbst aufgegeben.
Infolgedessen blieb nur der Start von Unit-Tests auf der Pull-Anfrage, wodurch wir die angegebenen 4 Minuten erreichen konnten. Build apk wird mit Merger Pull Request in dev ausgeführt.
Trotz der Tatsache, dass das Meeting fast 4 Stunden dauerte, konnten wir das brennende Problem der Organisation der Navigation in einem Projekt mit mehreren Modulen nicht diskutieren. Vielleicht ist dies das Thema für die nächsten Kaspersky Mobile Talks. Außerdem hat den Teilnehmern das Format sehr gut gefallen. Sagen Sie uns, worüber Sie in der Umfrage oder in den Kommentaren sprechen möchten.
Und schließlich nützliche Links aus demselben Chat: