当您的应用程序基于多模块体系结构构建时,您必须花费大量时间来确保模块之间的所有通信均正确地编写在代码中。 这项工作的一半可以交给Dagger 2框架,Android的Yandex.Map小组负责人Vladimir Tagakov
Noxa谈到了使用Dagger 2的多
模块性和DI在模块内部方便组织的利弊。
-我叫弗拉基米尔(Vladimir),我正在开发Yandex.Maps,今天我将向您介绍模块化和第二个Dagger。
我自己学习的时间最长,最快。 第二部分,我坐了几个星期,我将非常简洁地告诉您。

为什么我们开始在Maps中划分模块的困难过程? 我们只是想提高构建速度,众所周知。
目标的第二点是减少代码挂钩。 我从Wikipedia带走了装备。 这意味着我们希望减少模块之间的互连,以使模块分离并可以在应用程序外部使用。 问题的初步陈述:其他Yandex项目应该能够像我们一样完全使用Maps功能的一部分。 为了开发此功能,我们参与了该项目的开发。
我想向[k] apt扔一个燃烧的拖鞋,这会减慢组装速度。 我不太讨厌他,但我非常爱他。 他让我用匕首。

矛盾的是,模块分离过程的主要缺点是装配速度的降低。 尤其是在一开始,当您取出前两个模块(通用)和某些功能时,无论如何尝试,项目的总体构建速度都会下降。 最后,随着主模块中剩余的代码减少,构建速度将提高。 而且,这并不意味着一切都非常糟糕,有很多方法可以解决这个问题,甚至可以从第一个模块中获利。
第二个缺点是很难将代码分成模块。 曾经尝试过的人知道您将开始获取某种依赖关系,一些经典知识,最终将您将整个主模块复制到另一个模块并重新开始。 因此,您需要清楚地了解需要使用某种抽象来停止和断开连接的时刻。 缺点是更多的抽象。 更多抽象-更复杂的设计-更多抽象。
添加新的Gradle模块很困难。 怎么了 例如,开发人员来了,将新功能带入开发中,立即做得很好,制作了一个单独的模块。 怎么了 他必须记住所有在主模块中的可用代码,以便(如果有的话)重用并将其放在Common中。 因为在您的主App模块变成薄层之前,取出Common中的某些模块的过程是恒定的。
模块,模块,模块... Gradle模块,Dagger模块,接口模块太可怕了。

该报告将包括三个部分:小型,大型和复杂。 首先,实现与AGP中的API之间的区别。 Android Gradle Plugin 3.0相对较新。 他之前的一切怎么样?

这是一个健康的开发人员的典型项目,包括三个模块:主要模块App模块和组装在应用程序中的App模块,以及两个Feature模块。
立即谈论箭头。 这是一个很大的痛苦,每个人都朝着方便画画的方向画画。 对我而言,它们意味着从Core指向Feature的箭头。 因此,Feature了解Core,可以使用Core中的类。 如您所见,Core和App之间没有箭头,这意味着App似乎没有使用Core。 核心不是通用模块,它是每个人都依赖的模块,它是独立的,几乎没有代码。 虽然我们不会考虑。
我们的核心模块已更改,我们需要以某种方式重做。 我们更改其中的代码。 黄色-代码更改。

重新组装项目后。 很明显,在更改模块后,将必须对其进行重建,重新编译。 好吧

功能模块组装完成后,还取决于它。 显然,他的依赖性已经重新组合,您需要更新自己。 谁知道那里发生了什么变化。
最不愉快的事情就在这里发生。 尽管尚不清楚原因,但App模块正在运行。 我确定我不会以任何方式使用Core,并且不清楚为什么要重建该应用程序。 他很大,因为在开始的那一刻,这是非常大的痛苦。

另外,如果几个功能,许多模块都依赖Core,那么整个世界将被重新组装,这需要很长时间。
让我们升级到AGP的新版本,并按照手册中的说明,全部用API编译,而不是按照您的想法用Implementation编译。 没什么变化。 方案是相同的。 指定实现依赖项的新方法是什么? 想像一下仅使用此关键字而不使用API的相同方案? 它看起来像这样。

在这里的实现中,可以清楚地看到Core与App之间存在联系。 在这里,我们可以清楚地了解到我们不需要它,我们想摆脱它,因此只需将其删除即可。 一切都变得越来越容易。

现在几乎一切都很好,甚至还超过了。 如果我们在Core中更改某些API,添加新类,新的public或package private方法,则将重建Core和Feature。 如果您在方法内部更改实现或添加私有方法,则理论上完全不应该重建Feature,因为什么都没有改变。

让我们走得更远。 碰巧的是,许多人依赖我们的核心。 核心可能是某种网络或用户数据处理。 因为这是网络,所以每件事都经常变化,每件事都经过重新构建,并且我们也遭受了同样的痛苦,我们从中逃脱了。
让我们看看解决这个问题的两种方法。

我们只能将API从我们的核心模块转移到一个单独的模块,即我们使用的API。 在一个单独的模块中,我们可以取出这些接口的实现。

您可以在屏幕上查看连接。 Core Impl将无法使用这些功能。 也就是说,功能与Core实现之间没有任何联系。 以黄色突出显示的模块将仅提供工厂,该工厂将提供任何人都不知道的接口实现。
经过这样的转换后,我想提请您注意以下事实:由于API关键字是常驻的,因此Core API将可传递给所有功能。

完成这些转换后,我们会更改您最常执行的实现,并且仅会重建带有工厂的模块,它非常轻巧,很小,您甚至不必考虑所需的时间。

另一个选项并不总是有效。 例如,如果这是某种类型的网络,那么我几乎无法想象这将如何发生,但是,如果这是某种用户登录屏幕,则可能是这样。

我们可以制作Sample(与App相同的成熟根模块),并仅在其中收集一个功能,这将非常快,并且可以迭代快速地开发它。 在演示的结尾,我将向您展示构建和构建样本所花费的时间。
第一部分完成。 有哪些模块?

共有三种类型的模块。 常见的当然应该尽可能轻巧,并且不应包含任何功能,而应包含每个人都使用的功能。 对于我们团队中的这一点尤其重要。 如果我们将功能模块提供给其他应用程序,则无论如何都将迫使它们拖动“通用”。 如果他很胖,那么没人会爱我们。

如果您的项目较小,那么使用Common可以感觉更轻松,那么您也不必非常热心。

下一类模块是独立模块。 包含特定功能的最常见,最直观的模块:某种屏幕,某种用户脚本等。 它应尽可能独立,大多数情况下,您可以制作示例应用程序并在其中进行开发。 在拆分过程开始时,示例应用程序非常重要,因为一切仍在缓慢构建,并且您希望尽快获得利润。 最后,将所有内容分解为模块时,您可以重建所有内容,这将很快。 因为它将不会再次重建。

名人模块。 我自己想到了这个词。 关键是他对每个人都非常有名,许多人都依赖他。 相同的网络。 我已经说过,如果您经常重新组装它,如何避免一切都由您重新组装的事实。 还有另一种方法可用于小型项目,但不值得将所有内容作为单独的沉迷,单独的工件分发出去。

看起来像什么? 我们重复一遍,将API从名人中移除,取出其实现,然后小心翼翼,注意从Feature到Celebrity的箭头。 这正在发生。 您的模块的API属于“通用”,其实现保留在其中,提供该API实现的工厂出现在您的主模块中。 如果有人观看了Mobius,则Denis Neklyudov谈到了它。 非常相似的方案。
我们喜欢在项目中使用Dagger,我们喜欢它,并且我们想在不同模块的情况下最大程度地利用此好处。

我们希望每个模块都有一个独立的依赖图,有一个特定的根组件,您可以从中进行任何操作,我们希望每个Gradle模块都有自己的生成代码。 我们不希望生成的代码渗入主要代码。 我们想要尽可能多的编译时验证。 我们遭受[k]的困扰,至少我们应该从Dagger的收益中获利。 有了这些,我们不想强迫任何人使用Dagger。 无论是单独实现新功能模块的人,还是后来消费新功能模块的人,都不是我们自己为自己寻求一些功能的同事。
如何在我们的功能模块内部组织一个独立的依赖图?

您可以尝试使用Subcomponent,它甚至可以工作。 但这有很多缺陷。 您可以看到在Subcomponent中尚不清楚它从Component使用哪个依赖项。 要理解这一点,您必须漫长而痛苦地重新组装该项目,查看Dagger发誓要添加的内容。
此外,子组件的排列方式使它们迫使其他人使用Dagger,如果您决定拒绝某个模块,那么它对您的客户和您自己都不容易解决。

最令人作呕的事情之一是,当使用Subcomponent时,所有依赖项都被拉入主模块。 Dagger的设计使子组件由其框架组件的父类的嵌入式类生成。 也许有人在看生成的代码及其生成组件的大小? 我们有2万行。 由于子组件始终是组件的嵌套类,因此事实证明子组件的子组件也被嵌套,并且所有生成的代码都落入主模块中,这个需要编译并需要重构的20行文件,Studio开始放慢速度-痛苦。
但是有一个解决方案。 您可以只使用Component。

在Dagger中,组件可以指定依赖项。 这在代码中显示,并在图片中显示。 在其中指定Provision方法的依赖关系,这些工厂方法显示了组件所依赖的实体。 他想要它们在创作时。
以前,我一直认为只能在这些依赖项中指定其他组件,这就是原因-文档如此说。

现在,我了解了使用组件接口的含义,但是在我以为这只是一个组件之前。 实际上,您需要使用根据规则创建的接口,以为组件创建接口。 简而言之,当您只需要某种类型的依赖的获取器时,就可以使用Provision方法。 您还可以在Dagger文档中找到示例代码。

OtherComponent也写在这里,这很混乱,因为实际上,您不仅可以在其中插入组件。
我们想如何在现实中使用这项业务?

实际上,这里有一个Feature模块,它有一个可见的API包,位于所有包的根附近,并且说有一个入口点-FeatureActivity。 不必为了明确起见而使用typealias。 它可以是片段,也可以是ViewController-没关系。 并且有其依赖项FeatureDeps,表明他需要一个上下文,一些网络服务,来自Common,您想要从App中获得某种东西,任何客户端都必须满足此要求。 当他这样做时,一切都会起作用。

我们如何在功能模块中使用所有这些功能? 在这里我使用Activity,这是可选的。 像往常一样,我们创建自己的根Dagger组件并使用魔术方法findComponentDependencies,它与Android的Dagger非常相似,但主要由于我们不想拖动子组件,因此不能使用它。 否则,我们可以从中获取所有逻辑。
最初,我试图告诉它如何工作,但是您可以在星期五的示例项目中看到它。 库客户端在主模块中应如何使用它?

首先,它只是typealias。 实际上,它具有不同的名称,但是为了简洁起见,它却是。 MapOfDepth by Dependency接口类为您提供了其实现。 在App中,我们说可以像在Android中的Dagger中那样使用依赖项,并且组件继承此接口并自动接收Provision方法非常重要。 从这一刻开始,匕首开始迫使我们提供这种依赖性。 在您提供它之前,它将无法编译。 这是主要的便利:您决定安排一个功能,并使用此接口扩展组件-一切都做完,直到您完成其余工作,它不仅会编译,还会产生清晰的错误消息。 该模块很简单,关键是它将组件绑定到接口的实现。 与Android的Dagger大致相同。
让我们继续研究结果。

在关闭所有可能的功能之前,我检查了主机和本地笔记本电脑。 如果我们向Feature添加一个公共方法,则构建时间会明显不同。 在这里,我展示了构建示例项目时的差异。 这是16秒。 或者,当我收集所有卡片时-这意味着需要坐两分钟,等待每一次甚至最小的零钱。 因此,我们开发了许多功能,并将在示例项目中开发这些功能。 在大型机上,时间是可比的。

另一个重要结果。 在突出显示功能模块之前,它看起来像这样:在大型机上是28秒,现在是49秒。 我们已经分配了第一个模块,并且几乎已经两次接受了组装的减速。

另一种选择是对模块进行简单的增量组装,而不是像上一个那样增加功能。 直到分配模块为止28秒。 当我们分配了一个不需要每次都重建的代码,并且分配了[k] apt并不需要每次执行的代码时,我们赢得了三秒钟。 上帝知道什么,但是我希望每个新模块只会减少时间。
这里是文章的有用链接:
API与实现 ,
带有构建时间度量的文章 ,
示例模块 。 演示文稿将
可用 。 谢谢啦