📜 ⬆️ ⬇️

Kaspersky Mobile Talks # 1. Multi-modality

At the end of February, we launched a new meeting format for Android developers of Kaspersky Mobile Talks . The main difference from ordinary mitapes is that here, instead of hundreds of listeners and beautiful presentations, “experienced” developers gathered on several different topics to discuss just one topic: how they implement multi-modularity in their applications, what problems they face, and how they solve them.



Content


  1. Prehistory
  2. Mediators in HeadHunter. Alexander Blinov
  3. Domain modules in Tinkoff. Vladimir Kokhanov, Alexander Zhukov
  4. Impact analysis in Avito. Evgeny Krivobokov, Mikhail Yudin
  5. As in Tinkoff, the build time for PR was reduced from forty minutes to four. Vladimir Kokhanov
  6. useful links


Before turning to the direct content of the last meeting in the office of Kaspersky Lab, let us recall where the mod for the division of the application into modules came from (hereinafter the module is the Gradle module, not the Dagger, if not stated otherwise).


The topic of multi-modality is in the heads of the Android community for more than a year. One of the fundamental can be considered the report of Denis Neklyudov at last year in St. Petersburg "Mobius". He proposed to divide the monolith of the application, which has long ceased to be a thin client, into modules to increase the speed of assembly.
Link to report: Presentation , Video


Then there was a report by Vladimir Tagakov from Yandex.Maps about the connection of modules with the help of Dagger. Thus, they solve the problem of identifying a single component of maps for reuse in many other applications of Yandex.
Link to report: Presentation , Video


Kaspersky Lab also did not stay away from the trend: in September, Yevgeny Matsyuk wrote an article on how to connect modules with Dagger and at the same time build a multi-module architecture horizontally, not forgetting to follow the principles of Clean Architecture vertically.
Link to article


And on the winter "Mobius" there were two reports at once. First, Alexander Blinov talked about multi-modularity in the HeadHunter application using Toothpick as a DI, and right after him Artem Zinnatulin told about the pain from 800+ modules in Lyft. Sasha began to talk about multi-modularity, as a way to improve the architecture of the application, and not just to speed up the assembly.
Report Blinova: Presentation , Video
Zinnatulin Report: Video


Why did I start an article with a retrospective? Firstly, it will help you to better study the topic if you read about multi-modularity for the first time. And secondly, the first speech at our meeting began with a mini-presentation by Alexei Kalayda from Stream, who showed how they split their application into modules based on Zhenya's article (and some points seemed to me similar to Vladimir’s approach).


The main feature of this approach is binding to the UI: each module is connected as a separate screen - a fragment to which dependencies are transmitted from the main app-module, including the FragmentManager. At first, colleagues tried to implement multi-modality through proxy injectors, which Zhenya proposed in the article. But this approach seemed to be heavy: problems arose when one feature depended on another, which, in turn, depended on a third one — you had to write a proxy injector for each feature module. The approach based on UI components allows us not to write any injectors, allowing dependencies at the dependency level of target fragments.


The main limitations that this implementation has are: the feature must be a fragment (or another view); the presence of nested fragments, which leads to a large boilerplate. If a feature implements other features, it should be added to the dependency map, which Dagger checks when compiling. When there are many such features, difficulties arise at the moment of linking the dependency graph.



After Alexey’s report, Alexander Blinov took the floor. In his opinion, an UI-bound implementation would be suitable for DI containers in Flutter. Then the discussion turned to the discussion of multi-modularity in HeadHunter. The purpose of their division into modules was the possibility of architectural isolation of features and an increase in the speed of assembly.


Before dividing into modules it is important to prepare. First, you can build a dependency graph - for example, using such a tool . This will help to isolate the components with the minimum number of dependencies and get rid of the extra ones (chop). Only then can the least related components be allocated to modules.


Alexander recalled the main points about which he spoke in more detail on the Mobius. One of the challenges that the architecture must take into account is the reuse of a single module from different places in the application. In the example with the hh-application, this is the resume module, which should be accessible both to the vacancy list module (VacanciesList) when the user goes to the resume that he submitted for this vacancy, and to the Negotiation module. For clarity, I redraw the picture that Sasha depicted on a flipchart.



Each module contains two main entities: Dependencies - dependencies that this module needs, and API - methods that the module provides to the outside, to other modules. The connection between the modules is carried out by mediators, which are a flat structure in the main app-module. Each feature corresponds to one mediator. The mediators themselves are included in the composition of a certain MediatorManager in the project's app module. In code, it looks like this:


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 promised to soon publish a plugin to create modules in Android Studio, which is used to get rid of copy-paste in their company, as well as an example of a console multi-module project.


Some more facts about the current results of the division into modules of the application hh:




The following took the floor Alexander and Vladimir from "Tinkoff":
The scheme of their multi-module architecture looks like this:


Modules are divided into two categories: feature-modules and domain-modules.
Feature modules contain business logic and UI features. They depend on the domain-modules, but can not depend on each other.


Domain-modules contain code for working with data sources, that is, some models, DAO (for working with the database), API (for working with the network) and repositories (combine the work of the API and DAO). Domain modules, unlike feature modules, can depend on each other.


Communication between the domain- and feature-modules occurs entirely within the feature-modules (that is, in the terminology hh, Dependecies and API dependencies of the Domain-modules are completely resolved in the feature modules using them, without the use of additional mediator-type entities).


This was followed by a series of questions, which I will put here almost without any changes in the "question-answer" format:


- How is your authorization done? How do you drag it into feature modules?
- Features we do not depend on authorization, because almost all actions of the application occur in the authorized zone.

- How to track and clear unused components?
- We have such an entity as InjectorRefCount (implemented through WeakHashMap), which, when deleting the last Activity (or fragment) using this component, deletes it.

- How to measure “clean” scan and build time? If the caches are enabled, it turns out to be a rather dirty scan.
- You can disable Gradle Cache (org.gradle.caching in gradle.properties).
')
- How to run Unit tests from all modules in debug mode? If you just run gradle test, the tests from all flavors and buildType are pulled.
(This question caused a discussion of many participants of the meeting.)
- You can try to run testDebug.
- Then the modules for which there is no debug-configuration will not catch up. It starts either too much or too little.
- You can write the Gradle task, which for such modules will override testDebug, or make a fake debug configuration in the module's build.gradle.
- Implement this approach can be something like this:

 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") } 



The next improvised presentation was made by Evgeny Krivobokov and Mikhail Yudin from Avito.
They used mindmap to visualize their story.


Now in the company's project there are> 300 modules, while 97% of the code base is written in Kotlin. The main purpose of the breakdown into modules was to speed up the assembly of the project. The division into modules occurred gradually, with the selection of the least dependent parts of the code into modules. For this purpose, a tool has been developed for marking the dependencies of sources in the graph for impact analysis ( report on impact analysis at Avito ).


With this tool, you can mark a feature module as final so that other modules cannot depend on it. This property will be checked during impact-analysis and provides for the designation of explicit dependencies and agreements with the teams that are responsible for the module. On the basis of the constructed graph, the propagation of changes is also checked to launch unit tests for the affected code.


The company uses a mono-repository, but only for Android sources. The code of other platforms lives separately.


Gradle is used to build the project (although colleagues are already thinking about a more suitable for multi-module projects collector such as Buck or Bazel ). They have already tried Kotlin DSL, and then returned to Groovy in the Gradle scripts, because it is inconvenient to support different versions of Kotlin in Gradle and in the project - they put the common logic into plugins.


Gradle is able to parallelize tasks, cache, not reassemble binary dependencies, if their ABI has not changed, which accelerates the assembly of a multi-module project. For more efficient caching, Mainfraimer and several custom solutions are used:



All this helps to reach 15% cache miss on CI and 60-80% locally.


The following tips for working with Gradle can also be useful if a large number of modules appear in your project:




Vladimir from Tinkoff finished our meeting with the clickbate title of the report “How to reduce the build on PR from 40 minutes to four . ” In fact, we are talking about the distribution of gradle-task runs: apk assemblies, tests and static analyzers.


Initially, the guys on each pull-request were run static analysis, directly assembly and tests. This process took 40 minutes, of which only Lint and SonarQube took 25 and fell only on 7% of launches.


Thus, it was decided to bring their launch to a separate Job, which runs on a schedule every two hours and in case of an error sends a message to Slack.


The opposite was the situation with Detekt. He fell almost constantly, which is why he was taken out in a preliminary pre-push check.


So in the pull request verification only the apk assembly and unit tests remained. Tests compile sources before launch, but do not collect resources. Since the resources merge almost always ended in success, apk itself was also abandoned.


As a result, only the launch of unit tests remained on the pull request, which made it possible to achieve the indicated 4 minutes. Building apk is done when the pull request is merged into dev.



Despite the fact that the meeting lasted almost 4 hours, we did not have time to discuss the burning issue of organizing navigation in a multi-module project. Perhaps this is the theme for the next Kaspersky Mobile Talks. Moreover, the format is very much like the participants. Tell us in the survey or comments what you would like to talk about.


And finally, useful links from the same chat:


Source: https://habr.com/ru/post/446626/


All Articles