Fin février, nous avons lancé un nouveau format pour les réunions des développeurs Android de Kaspersky Mobile Talks . La principale différence avec les rassemblements ordinaires est qu'au lieu de centaines d'auditeurs et de belles présentations, les développeurs «expérimentés» se sont réunis sur plusieurs sujets différents pour discuter d'un seul sujet: comment ils implémentent la multi-modularité dans leurs applications, quels problèmes ils rencontrent et comment ils les résolvent.

Table des matières
- Contexte
- Médiateurs dans HeadHunter. Alexander Blinov
- Modules de domaine Tinkoff Vladimir Kokhanov, Alexander Zhukov
- Analyse d'impact dans Avito. Evgeny Krivobokov, Mikhail Yudin
- Comme à Tinkoff, ils ont réduit le temps de montage des RP de quarante minutes à quatre. Vladimir Kokhanov
- Liens utiles
Avant de passer au contenu immédiat de la réunion au bureau de Kaspersky Lab, rappelons d'où vient le mod pour diviser l'application en modules (ci-après, le module est considéré comme un module Gradle, pas un Dagger, sauf indication contraire).
Le sujet de la multi-modularité est dans l'esprit de la communauté Android depuis des années. L'un des éléments fondamentaux peut être considéré comme un rapport de Denis Neklyudov au "Mobius" de Saint-Pétersbourg l'année dernière. Il a proposé de diviser l'application monolithique, qui avait depuis longtemps cessé d'être un client léger, en modules pour augmenter la vitesse de construction.
Lien vers le rapport: présentation , vidéo
Ensuite, il y a eu un rapport de Vladimir Tagakov de Yandex.Maps sur la liaison des modules à l'aide de Dagger. Ainsi, ils résolvent le problème de l'allocation d'un seul composant de cartes à réutiliser dans de nombreuses autres applications Yandex.
Lien vers le rapport: présentation , vidéo
Kaspersky Lab ne s'est pas non plus éloigné de la tendance: en septembre, Evgeni Matsyuk a écrit un article sur la façon de connecter des modules à l'aide de Dagger et en même temps de construire une architecture multi-modules horizontalement, sans oublier de suivre verticalement les principes de l'architecture propre.
Lien vers l'article
Et l'hiver Mobius, il y avait deux rapports à la fois. Tout d'abord, Alexander Blinov a parlé de la multi-modularité dans l'application HeadHunter utilisant Toothpick comme DI, et juste après lui, Artem Zinnatulin a parlé de la douleur de plus de 800 modules dans Lyft. Sasha a commencé à parler de multi-modularité, comme un moyen d'améliorer l'architecture de l'application, et pas seulement d'accélérer l'assemblage.
Rapport Blinov: présentation , vidéo
Rapport Zinnatulin: Vidéo
Pourquoi ai-je commencé l'article par une rétrospective? Premièrement, cela vous aidera à mieux étudier le sujet si vous lisez pour la première fois sur la multi-modularité. Et deuxièmement, le premier discours de notre réunion a commencé par une mini-présentation d'Alexey Kalaida de la société Stream, qui a montré comment ils divisaient leur candidature en modules basés sur l'article de Zhenya (et certains points me semblaient similaires à l'approche de Vladimir).
La principale caractéristique de cette approche était la liaison à l'interface utilisateur: chaque module est connecté comme un écran séparé - un fragment auquel les dépendances sont transférées à partir du module d'application principal, y compris le FragmentManager. Tout d'abord, des collègues ont essayé de mettre en œuvre la multi-modularité via des injecteurs proxy, ce que Zhenya a proposé dans l'article. Mais cette approche semblait écrasante: il y avait des problèmes quand une fonctionnalité dépendait d'une autre, qui, à son tour, dépendait de la troisième - nous devions écrire un injecteur proxy pour chaque module de fonctionnalité. L'approche basée sur les composants de l'interface utilisateur vous permet de ne pas écrire d'injecteurs, ce qui permet des dépendances au niveau de la dépendance des fragments cibles.
Les principales limitations de cette implémentation: une fonctionnalité doit être un fragment (ou une autre vue); la présence de fragments imbriqués, ce qui conduit à un grand passe-partout. Si une fonctionnalité implémente d'autres fonctionnalités, elle doit être ajoutée à la carte de dépendances, que Dagger vérifie lors de sa compilation. Lorsqu'il existe de nombreuses fonctionnalités de ce type, des difficultés surviennent au moment de lier le graphique de dépendance.
Après le rapport d'Alexey, Alexander Blinov a pris la parole. À son avis, la mise en œuvre liée à l'interface utilisateur conviendrait aux conteneurs DI à Flutter. Ensuite, la discussion est passée à une discussion multi-modules dans HeadHunter. Le but de leur division en modules était la possibilité d'isoler l'architecture des caractéristiques et d'augmenter la vitesse d'assemblage.
Avant de se diviser en modules, il est important de se préparer. Vous pouvez d'abord créer un graphique de dépendance - par exemple, en utilisant un tel outil . Cela aidera à identifier les composants avec un nombre minimum de dépendances et à se débarrasser des composants inutiles (hacher). Ce n'est qu'après cela que les composants les moins connectés peuvent être sélectionnés en modules.
Alexander a rappelé les principaux points dont il a parlé plus en détail à Mobius. L'une des tâches complexes que l'architecture doit prendre en compte est la réutilisation d'un module à différents endroits de l'application. Dans l'exemple avec l'application hh, il s'agit d'un module de curriculum vitae, qui devrait être accessible à la fois au module de liste des postes vacants (VacanciesList), lorsque l'utilisateur accède au curriculum vitae qu'il a soumis pour ce poste vacant, et au module de réponse négative (négociation). Pour plus de clarté, j'ai redessiné l'image que Sasha représentait sur un tableau à feuilles.

Chaque module contient deux entités principales: les dépendances - les dépendances dont ce module a besoin et l'API - les méthodes que le module fournit vers l'extérieur aux autres modules. La communication entre les modules est effectuée par des médiateurs, qui sont une structure plate dans le module d'application principal. Chaque fonctionnalité a un choix. Les médiateurs eux-mêmes sont inclus dans un certain MediatorManager dans le module d'application du projet. Dans le code, cela ressemble à ceci:
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 a promis de publier bientôt un plug-in pour créer des modules dans Android Studio, qui est utilisé pour se débarrasser du copier-coller dans leur entreprise, ainsi qu'un exemple de projet multi-module de console.
Quelques faits supplémentaires sur les résultats actuels de la séparation du module d'application hh:
- ~ 83 modules de fonctionnalités.
- Pour effectuer un test A / B, les fonctions peuvent être entièrement remplacées par le module de fonction au niveau du médiateur.
- Le graphique de Gradle Scan montre qu'après la compilation des modules en parallèle, un processus assez long de dexing de l'application a lieu (dans ce cas, deux: pour les candidats et les employeurs):

Alexander et Vladimir de Tinkoff ont pris la parole:
Le schéma de leur architecture multi-modules ressemble à ceci:

Les modules sont divisés en deux catégories: les modules de fonctionnalité et les modules de domaine.
Les modules de fonctionnalités contiennent la logique métier et les fonctionnalités de l'interface utilisateur. Ils dépendent des modules de domaine, mais ne peuvent pas dépendre les uns des autres.
Les modules de domaine contiennent du code pour travailler avec des sources de données, c'est-à-dire certains modèles, DAO (pour travailler avec la base de données), API (pour travailler avec le réseau) et référentiels (combiner le travail de l'API et DAO). Les modules de domaine, contrairement aux modules de fonctionnalités, peuvent dépendre les uns des autres.
La connexion entre le domaine et les modules de fonctionnalités s'effectue entièrement à l'intérieur des modules de fonctionnalités (c'est-à-dire que dans la terminologie de hh, les dépendances et les dépendances API des modules de domaine sont entièrement résolues dans les modules de fonctionnalités qui les utilisent, sans l'utilisation d'entités supplémentaires telles que des médiateurs).
Cela a été suivi d'une série de questions, que je mettrai presque inchangées ici dans le format «question-réponse»:
- Comment se fait l'autorisation? Comment le faites-vous glisser dans les modules de fonctionnalités?
- Les fonctionnalités avec nous ne dépendent pas de l'autorisation, car presque toutes les actions de l'application se produisent dans la zone autorisée.
- Comment suivre et nettoyer les composants inutilisés?
- Nous avons une entité telle que InjectorRefCount (implémentée via WeakHashMap), qui, lors de la suppression de la dernière activité (ou fragment) utilisant ce composant, la supprime.
- Comment mesurer un scan «propre» et un temps de construction? Si les caches sont activés, un scan plutôt sale est obtenu.
- Vous pouvez désactiver Gradle Cache (org.gradle.caching dans gradle.properties).
- Comment exécuter des tests unitaires à partir de tous les modules en mode débogage? Si vous exécutez uniquement le test Gradle, les tests de toutes les versions et buildType sont extraits.
(Cette question a déclenché la discussion de nombreux participants à la réunion.)
- Vous pouvez essayer d'exécuter testDebug.
- Les modules pour lesquels il n'y a pas de configuration de débogage ne seront pas renforcés. Cela commence trop ou trop peu.
- Vous pouvez écrire une tâche Gradle, qui remplacera testDebug pour ces modules, ou faire une fausse configuration de débogage dans le build.gradle du module.
- Vous pouvez implémenter cette approche comme ceci:
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") }

La présentation improvisée suivante a été faite par Evgeny Krivobokov et Mikhail Yudin d'Avito.
Ils ont utilisé une carte mentale pour visualiser leur histoire.
Désormais, le projet de la société compte plus de 300 modules, dont 97% de la base de code est écrite en Kotlin. L'objectif principal de la décomposition en modules était d'accélérer le montage du projet. La répartition en modules s'est produite progressivement, les parties les moins dépendantes du code étant attribuées aux modules. Pour ce faire, un outil a été développé pour marquer les dépendances des codes sources dans le graphique pour l'analyse d'impact ( rapport sur l'analyse d'impact dans Avito ).
À l'aide de cet outil, vous pouvez marquer un module d'entités comme final afin que les autres modules ne puissent pas en dépendre. Cette propriété sera vérifiée lors de l'analyse d'impact et fournit une désignation des dépendances explicites et des accords avec les équipes responsables du module. Sur la base du graphique construit, la distribution des modifications est également vérifiée pour exécuter des tests unitaires pour le code affecté.
L'entreprise utilise un mono-référentiel, mais uniquement pour les sources Android. Le code des autres plateformes vit séparément.
Gradle est utilisé pour construire le projet (même si des collègues pensent déjà à un collectionneur comme Buck ou Bazel plus adapté aux projets multi-modules). Ils ont déjà essayé Kotlin DSL, puis sont revenus à Groovy dans les scripts Gradle, car il n'est pas pratique de prendre en charge différentes versions de Kotlin dans Gradle et dans le projet - la logique générale est mise dans les plugins.
Gradle peut paralléliser les tâches, mettre en cache et ne pas réassembler les dépendances binaires si leur ABI n'a pas changé, ce qui garantit un assemblage plus rapide d'un projet multi-module. Pour une mise en cache plus efficace, Mainfraimer et plusieurs solutions auto-écrites sont utilisées:
- Lors du passage d'une branche à l'autre, Git peut laisser des dossiers vides qui cassent la mise en cache ( problème Gradle # 2463 ). Par conséquent, ils sont supprimés manuellement à l'aide du crochet Git.
- Si vous ne contrôlez pas l'environnement sur les machines des développeurs, différentes versions du SDK Android et d'autres paramètres peuvent dégrader la mise en cache. Pendant la construction du projet, le script compare les paramètres d'environnement avec ceux attendus: si des versions ou des paramètres incorrects sont installés, la construction sera supprimée.
- Analytics est en marche / arrêt des paramètres et de l'environnement. C'est pour surveiller et aider les développeurs.
- Les erreurs de génération sont également envoyées aux analyses. Les problèmes connus et populaires sont saisis sur une page spéciale avec une solution.
Tout cela aide à atteindre 15% de cache manquant sur CI et 60 à 80% localement.
Les conseils Gradle suivants peuvent également être utiles si un grand nombre de modules apparaissent dans votre projet:
- La désactivation des modules via des drapeaux IDE n'est pas pratique; ces drapeaux peuvent être réinitialisés. Par conséquent, les modules sont désactivés via settings.gradle.
- Dans le studio 3.3.1, il y a une case à cocher «Ignorer la génération de source sur la synchronisation Gradle si un projet a plus de 1 modules». Par défaut, il est désactivé, il est préférable de l'activer.
- Les dépendances sont enregistrées dans buildSrc pour être réutilisées dans tous les modules. Une autre option est Plugins DSL , mais vous ne pouvez pas mettre l'application du plugin dans un fichier séparé.
Notre réunion s'est terminée avec Vladimir de Tinkoff avec le titre de clickbait du rapport "Comment réduire l' Assemblée sur PR de 40 minutes à quatre" . En fait, nous parlions de la distribution des démarrages de gradle-plugs: builds apk, tests et analyseurs statiques.
Initialement, les gars à chaque demande de pull ont effectué une analyse statique, directement l'assemblage et les tests. Ce processus a duré 40 minutes, dont seuls Lint et SonarQube ont pris 25 minutes et n'ont perdu que 7% des lancements.
Ainsi, il a été décidé de placer leur lancement dans un Job séparé, qui s'exécute selon un planning toutes les deux heures et, en cas d'erreur, envoie un message à Slack.
La situation inverse était d'utiliser Detect. Il s'est écrasé presque constamment, c'est pourquoi il a été soumis à un contrôle préliminaire préalable.
Ainsi, seuls l'assemblage apk et les tests unitaires sont restés dans la vérification de la demande d'extraction. Les tests compilent les sources avant de s'exécuter, mais ne collectent pas de ressources. Comme la fusion des ressources a presque toujours réussi, l'assemblage apk lui-même a également été abandonné.
En conséquence, seul le lancement des tests unitaires est resté sur la demande de pull, ce qui nous a permis d'atteindre les 4 minutes indiquées. Build apk est effectué avec une demande de fusion pull en dev.
Malgré le fait que la réunion a duré près de 4 heures, nous n'avons pas réussi à discuter de la question brûlante de l'organisation de la navigation dans un projet multi-modules. C'est peut-être le sujet des prochains Kaspersky Mobile Talks. De plus, les participants ont vraiment aimé le format. Dites-nous de quoi vous aimeriez parler dans le sondage ou dans les commentaires.
Et enfin, des liens utiles du même chat: