2月下旬,我们为Kaspersky Mobile Talks的Android开发人员的会议推出了一种新格式。 与普通集会的主要区别在于,“有经验的”开发人员不是数百名听众和漂亮的演讲,而是聚集在几个不同的主题上,仅讨论一个主题:他们如何在应用程序中实现多模块性,面临的问题以及如何解决这些问题。

目录内容
- 背景知识
- HeadHunter中的调解员。 亚历山大·布利诺夫(Alexander Blinov)
- Tinkoff域模块 弗拉基米尔·科哈诺夫(Vladimir Kokhanov),亚历山大·朱可夫(Alexander Zhukov)
- Avito中的影响分析。 叶夫根尼·克里沃博科夫(Evgeny Krivobokov),米哈伊尔·尤丁(Mikhail Yudin)
- 与在Tinkoff中一样,他们将PR的组装时间从40分钟减少到4分钟。 弗拉基米尔·科哈诺夫(Vladimir Kokhanov)
- 有用的链接
在继续进行卡巴斯基实验室办公室会议的即时内容之前,让我们回想一下mod的来源,该模块来自于将应用程序划分为多个模块(在下文中,除非另有说明,否则该模块应理解为Gradle模块,而不是Dagger)。
多年以来,多模块化一直是Android社区关注的话题。 Denis Neklyudov在去年圣彼得堡“莫比乌斯”(Mobius)的一份报告中可以认为是基本面之一。 他建议将早已不再是瘦客户机的单片应用程序划分为多个模块,以提高构建速度。
链接到报告: 演示 , 视频
接下来是Yandex.Maps的Vladimir Tagakov的报告,内容涉及使用Dagger链接模块。 因此,它们解决了分配卡的单个组件以在许多其他Yandex应用程序中重用的问题。
链接到报告: 演示 , 视频
卡巴斯基实验室也不甘落后:9月,Evgeni Matsyuk写了一篇文章,介绍如何使用Dagger连接模块,同时水平构建多模块架构,不要忘记垂直遵循Clean Architecture的原则。
链接到文章
在冬季的莫比乌斯(Mobius),一次有两个报道。 首先,亚历山大·布利诺夫(Alexander Blinov)谈到了使用Toothpick作为DI的HeadHunter应用程序中的多模块性,而紧随其后的是Artem Zinnatulin谈到了Lyft中800多个模块的痛苦。 Sasha开始谈论多模块化,这是一种改进应用程序体系结构的方法,而不仅仅是提高组装速度。
Blinov报告: 演示 , 视频
Zinnatulin报告: 视频
为什么我要回顾性地开始这篇文章? 首先,如果您是第一次阅读有关多模块性的知识,它将有助于您更好地研究该主题。 其次,在我们会议上的第一场演讲以Stream公司的Alexey Kalaida的迷你演讲开始,展示了他们如何根据Zhenya的文章将应用程序划分为模块(在我看来有些观点类似于Vladimir的方法)。
这种方法的主要功能是绑定到UI:每个模块都作为一个单独的屏幕连接-从主应用程序模块(包括FragmentManager)将依赖项传输到的片段。 首先,同事们尝试通过代理注入器实现多模块性,这是振亚在文章中提出的。 但是这种方法似乎势不可挡:当一个功能依赖于另一个功能而又依赖于第三个功能时,就会出现问题-我们必须为每个功能模块编写一个代理注入器。 基于UI组件的方法允许您不编写任何注入程序,而允许在目标片段的依赖级别进行依赖。
此实现的主要限制是:功能必须是片段(或其他视图); 嵌套片段的存在会导致大型样板。 如果某个功能实现了其他功能,则应将其添加到依赖关系图,Dagger会在编译依赖关系图时对其进行检查。 当具有许多这样的特征时,在链接依赖图时会出现困难。
在阿列克谢的报告之后,亚历山大·布利诺夫(Alexander Blinov)发言。 他认为,与UI绑定的实现适用于Flutter中的DI容器。 然后,讨论切换到HeadHunter中的多模块讨论。 将它们划分为模块的目的是实现结构上的特征隔离并提高组装速度。
在划分模块之前,进行准备很重要。 首先,您可以构建一个依赖图-例如,使用这样的工具 。 这将有助于以最小数量的依赖关系隔离组件,并消除不必要的组件(斩波)。 只有这样,才能将连接最少的组件选择为模块。
亚历山大回顾了他在莫比乌斯(Mobius)上更详细谈到的要点。 架构必须考虑的复杂任务之一是从应用程序的各个位置重用一个模块。 在hh应用程序的示例中,这是一个简历模块,当用户转到他为此空缺提交的简历时,空缺列表模块(VacanciesList)和否定响应模块(协商)都可以访问该模块。 为了清楚起见,我重新绘制了Sasha在活动挂图上描绘的图片。

每个模块包含两个主要实体:依赖关系-该模块需要的依赖关系,以及API-该模块向外提供给其他模块的方法。 模块之间的通信由调解器执行,调解器是主应用程序模块中的平面结构。 每个功能都有一个选择。 调解器本身包含在项目应用程序模块的某个MediatorManager中。 在代码中,它看起来像这样:
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 } }
亚历山大承诺将很快发布一个插件,用于在Android Studio中创建模块,该插件可用来消除公司中的复制粘贴,并提供控制台多模块项目的示例。
有关hh应用程序模块分离的当前结果的更多事实:
- 〜83个功能模块。
- 要进行A / B测试,可以在介体级别完全用功能模块替换功能。
- Gradle Scan的图表显示,在并行编译模块之后,将应用程序进行解密的过程相当漫长(在这种情况下,两个过程:适用于申请人和雇主):

以下是Tinkoff的Alexander和Vladimir的发言:
他们的多模块体系结构的方案如下所示:

模块分为两类:功能模块和域模块。
功能模块包含业务逻辑和UI功能。 它们依赖域模块,但不能相互依赖。
域模块包含用于处理数据源的代码,即某些模型,DAO(用于数据库),API(用于网络)和存储库(将API和DAO的工作结合在一起)。 域模块与功能模块不同,可以相互依赖。
域和功能模块之间的连接完全在功能模块内部进行(也就是说,在hh的术语中,使用功能模块在功能模块中完全解决了域模块的Dependecies和API依赖关系,而无需使用中介程序等其他实体)。
接下来是一系列问题,在这里我将以“问题-答案”的格式保持不变:
-授权如何完成? 如何将其拖动到功能模块中?
-我们的功能不依赖于授权,因为应用程序的几乎所有操作都发生在授权区域中。
-如何跟踪和清洁未使用的组件?
-我们具有InjectorRefCount这样的实体(通过WeakHashMap实现),当使用该组件删除最后一个Activity(或片段)时,会将其删除。
-如何衡量“干净”的扫描和建立时间? 如果打开了缓存,则会获得较脏的扫描。
-您可以禁用Gradle缓存(gradle.properties中的org.gradle.caching)。
-如何在调试模式下从所有模块运行单元测试? 如果仅运行gradle测试,则会提取所有版本和buildType的测试。
(此问题引发了会议上许多参与者的讨论。)
-您可以尝试运行testDebug。
-然后,没有调试配置的模块将不会拧紧。 它开始得太多或太少。
-您可以编写Gradle任务,该任务将覆盖此类模块的testDebug,或在模块build.gradle中进行伪调试配置。
-您可以像这样实现此方法:
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") }

下一个即席演示是由Avito的Evgeny Krivobokov和Mikhail Yudin进行的。
他们使用思维导图来形象化他们的故事。
现在,该公司的项目有> 300个模块,其中97%的代码库是用Kotlin编写的。 分解为模块的主要目的是加快项目的组装。 逐步分解为模块,将最少相关代码部分分配给模块。 为此,开发了一种工具,用于在图形中标记源代码的依存关系以进行影响分析( 有关Avito中的影响分析的报告 )。
使用此工具,可以将功能模块标记为最终模块,以便其他模块不能依赖它。 在影响分析期间将检查此属性,并指定与负责该模块的团队的明确依赖关系和协议。 基于构造的图,还检查更改的分布,以运行受影响代码的单元测试。
该公司使用单一存储库,但仅适用于Android来源。 其他平台的代码分别存在。
Gradle用于构建项目(尽管同事已经在考虑像Buck或Bazel这样的收集器更适合于多模块项目)。 他们已经尝试过Kotlin DSL,然后使用Gradle脚本返回Groovy,因为在Gradle和项目中支持不同版本的Kotlin不方便-通用逻辑被插入插件中。
如果Gradle的ABI不变,则Gradle可以并行化任务,缓存并且不重新编译二进制依赖项,从而确保更快地组装多模块项目。 为了提高缓存效率,使用了Mainfraimer和一些自写的解决方案:
- 当从一个分支切换到另一个分支时,Git可能会留下空的文件夹来破坏缓存( Gradle问题#2463 )。 因此,它们是使用Git挂钩手动删除的。
- 如果您不控制开发人员计算机上的环境,则不同版本的Android SDK和其他参数可能会降低缓存的性能。 在项目的构建过程中,脚本会将环境参数与预期的参数进行比较:如果安装了错误的版本或参数,构建将被删除。
- Analytics(分析)会开启/关闭参数和环境。 这是为了监视和帮助开发人员。
- 构建错误也将发送到分析。 已知问题和常见问题将在带有解决方案的特殊页面上输入。
所有这些有助于在CI上实现15%的高速缓存未命中,在本地实现60-80%的高速缓存未命中。
如果项目中出现大量模块,则以下Gradle提示也可能很有用:
- 通过IDE标志禁用模块很不方便;可以重置这些标志。 因此,可通过settings.gradle禁用模块。
- 在Studio 3.3.1中,有一个复选框“如果项目具有多个模块,则在Gradle同步上跳过源代码生成”。 默认情况下它是关闭的,最好打开它。
- 依赖关系在buildSrc中注册,以便在所有模块中重用。 另一个选项是Plugins DSL ,但是您不能将插件的应用程序放在单独的文件中。
我们的会议以Tinkoff的Vladimir结束,报告的标题为“如何将PR的组装时间从40分钟减少到4分钟” 。 实际上,我们在谈论gradle-plugs的开始分布:apk构建,测试和静态分析器。
最初,每个请求请求的人员都进行了静态分析,直接进行了组装和测试。 这个过程耗时40分钟,其中只有Lint和SonarQube花了25分钟,仅降落了发射次数的7%。
因此,决定将它们的启动放在一个单独的Job中,该Job每两小时按计划运行一次,如果发生错误,则向Slack发送一条消息。
相反的情况是使用检测。 它几乎不断崩溃,这就是为什么要对其进行初步的预推检查。
因此,在拉取请求验证中仅保留了apk组装和单元测试。 测试在运行之前会编译源,但不会收集资源。 由于资源合并几乎总是成功,因此apk程序集本身也被放弃了。
结果,拉动请求上仅保留了单元测试的启动,这使我们得以完成指示的4分钟。 构建apk是在dev中与合并请求合并进行的。
尽管会议持续了将近4个小时,但我们仍未能讨论在多模块项目中组织导航这一迫在眉睫的问题。 也许这是下一次卡巴斯基手机技术讲座的主题。 此外,参与者真的很喜欢这种格式。 在调查或评论中告诉我们您想谈什么。
最后,来自同一聊天的有用链接: