前面已经讲了 Dagger 的基础注解,并且最后我们也搭建了一个最简单的 Dagger 注入。
这一篇我们继续学习 Dagger 更多的注解,以及如何模块化地管理。这些将帮助我们妥善组织不同的组件、明确各自的生命周期。
@Named
依赖注入迷失
之前说过 @Module
和 @Provides
配合可以包装没有 @Inject
标注的构造函数。但如果包装了一个已经有了 @Inject
的类会怎么样?其实这俩有优先级的。Dagger 会优先从 Module 中查找实例化方法,如果找不到再去找被 Inject
的标记的构造函数。 这也非常好理解,一般人肯定会选择优先去超市买东西,而不是直接去拜访工厂。
一般来说,为了便于管理,我们会统一用 Module 封装一层,无论构造函数有没有被标注。这可以帮助我们更好地管理依赖结构与生命周期,这些后面会讲到。
但如果 Module 里有两个返回值类型一样的 Provides 呢?考虑下面的代码:
class Stove() {
var name: String? = null
constructor(name: String) : this() {
this.name = name
}
}
@Module
class MainModule() {
@Provides
provideStove():Stove {
return Stove()
}
@Provides
provideStove():Stove { // 现在有两个Provides都返回炉子
return Stove("Boom")
}
}
现在家乐福里有两个炉子,Dagger 不知道该买哪一个,我们给这种情况起个名字叫「依赖注入迷失」。依赖注入迷失会在编译期报错,很容易发现。
解决它
为了解决这个问题,必须引入一个新的注解 @Named
,也就是厨师会指明到底需要哪个型号的炉子,这样就不会买错了。同时,记得给超时货架上的炉子也表明型号,不然怎么买对吧 -。-
改造后的 Module 与 Chef 如下:
@Module
class MainModule() {
@Provides
@Named("noname")
provideStove():Stove {
return Stove()
}
@Provides
@Named("boom")
provideStove():Stove { // 现在有两个Provides都返回炉子
return Stove("Boom")
}
}
class Chef() {
@Inject
@Named("noname")
val stove1: Stove
@Inject
@Named("boom")
val stove2: Stove
}
我们的厨师比较贪婪,他两个型号全都要。但与一开始胡乱买不同,现在他清楚地指明了我需要两个型号,并且能分清这两个型号。于是就不会报错了。
@Qualifier
Qualifier
与 Named
的作用一模一样。只不过 Named 是用单纯的字符串区分,而 Qualifier 需要先自定义注解。现在我们把刚才的例子改用 Qualifier 实现。
// 定义一个新的注解,名叫 StoveQualifier
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class StoveQualifier
@Module
class MainModule() {
@Provides
@StoveQualifier("noname")
provideStove():Stove {
return Stove()
}
@Provides
@StoveQualifier("boom")
provideStove():Stove { // 现在有两个Provides都返回炉子
return Stove("Boom")
}
}
class Chef() {
@Inject
@StoveQualifier("noname")
val stove1: Stove
@Inject
@StoveQualifier("boom")
val stove2: Stove
}
看到没,和 Named 用法一模一样对吧。肯定有人要问,既然那么麻烦问什么不直接用 Named 呢。
你可以把 Qualifier 看做是自定义命名空间。之前所有的型号都标注在 Named 空间下。也就是空调、炉子、电磁炉、冰箱等等,型号全部糅杂在一起,显然这不是个好办法。通过自定义 Qualifier,我们可以让每个类有自己的型号命名空间,不要担心冲突与混淆了。
模块化管理
一开始已经提到,为了便于管理我们会统一用 Module 封装一层,而 Module 最终要被关联到 Component。因此问题的关键就成了该如何组织 Component。
划分原则
既然标题叫 Dagger2 in Android,自然是要重点考虑 Android 上面的应用。一个思维正常的程序猿都不会把所有注入都写进一个 Component,否则会变得非常庞大、难以维护。但是划分的粒度也不可以太小,如果为每个类都创建一个 Component,也会变得非常复杂、难以维护。
让我们回到一开始 Dagger 到底是干什么用的?经过一轮学习相信大家都有自己的答案。我认为它主要作用是「创建并管理对象,将其注入到需要它们的类」。既然是管理对象,那就不得不考虑生命周期。因此基于生命周期的划分也许是个不错的点子。
一个 Android 应用有很多生命周期,大致有两类:
- Application:这是最长的生命周期,从我们应用启动开始,直到被彻底销毁。
- Activity/Fragment: 都表示一个页面。打开时开始,离开时销毁。
所以我们完全可以按照生命周期来对 Component 进行划分。
组织 Component
我们知道 Component 本质就是一个接口(抽象类),因此它互相也可以有联系,关系分为两种:依赖关系与包含关系。
依赖关系(组件依赖)
现在我们有两个 Component,分别是 AppComponent 与 ActivityComponent,前者持有一个全局 Context 对象,我们希望后者依赖前者。那么可以这么做:
@Module
class AppModule(private val context: Context) {
@Provides
fun provideContext(): Context = context
}
@Component(modules = [AppModule::class])
interface AppComponent {
fun context(): Context // 注意这行
}
@Module
class ActivityModule {
@Provides
fun provideSp(context: Context) =
context.getSharedPreferences("Cooker", Context.MODE_PRIVATE)
}
// 声明了依赖关系
@Component(dependencies = [AppComponent::class], modules = [ActivityModule::class])
interface ActivityComponent {
}
分析一下这段代码:
ActivityModule 定义了一个 Provides 能够返回 SharedPreferences 的实例。但是创建这个实例需要 context,它是哪来的?由于它声明了依赖 AppComponent,而 AppComponent 拥有的 AppModule 中有可以提供 context 的 Provides,因此 ActivityModule 从 AppComponent 那里拿到了 context。
但这不是无条件的,依赖别人的前提是别人愿意被你依赖才行。因此 AppComponent 中必须显示地定义一个能够返回 Context 类型的函数,依赖它的 Component 才能拿到。如果不定义,即使有,也不会给别人的。
注意区分 Component 中的函数与 Module 中 Provides 的区别:前者作用是:① 用于注入 ② 用于给依赖的 Component 提供对象;后者作用仅仅是创建对象。
包含关系(子组件)(组件继承)
依赖就像朋友,对方愿意才可以分享。包含就像父母,分享是无条件的。
声明继承需要以下几步
- 子 Component 用
@Subcomponent
注解。 - 子 Component 声明一个 Builder 来告诉父 Component 如何创建自己。
- 父 Component 对应的 Module 用
subcomponents
属性来指明拥有哪些子 Component。 - 父 Component 声明一个抽象方法来获取子 Component 的 Builder。
上面的例子用包含关系可以这样改写:
@SubComponent(modules = [ActivityModule::class]) // 子Component用@Subcomponent注解。
interface ActivityComponent {
// 声明一个Builder来告诉父Component如何创建自己
@Subcomponent.Builder
interface Builder {
fun build(): ActivityComponent
}
}
// 父Component对应的Module用subcomponents属性来指明拥有哪些子Component
@Module(subcomponents = [ActivityComponent::class])
class AppModule(private val context: Context) {
@Provides
fun provideContext(): Context = context
}
@Component(modules = [AppModule::class])
interface AppComponent {
//fun context(): Context // 不需要显示定义了
// 父Component声明一个抽象方法来获取子Component的Builder
fun activityComponent(): ActivityComponent.Builder
}
声明包含关系后,父接口所能提供的所有对象子接口下的 Module 都可以直接使用,不再需要显示声明了。
对于包含关系,子 Component 将不再生成 DaggerXxxComponent 类,需要通过父 Component 的实例来创建子 Component。
对比
相同点:
- 都可以使用父接口所提供的对象。
不同点:
- 生成代码不同。依赖关系每一个 Component 都会生成一个 DaggerXxxComponent 类;而包含关系只会生成一个。
- 对父接口对象访问限制不同。依赖关系必须主动声明才能获取到;包含关系默认能获取到。
那么究竟选用哪个,似乎没有准确的规范,在更多的实践中体会吧。(一般在 Android 中,会让 Activity 包含于 AppComponent)
总结
这一章主要学习了 Dagger 的模块化管理。一开始提到过,Dagger 还可以管理对象的生命周期,这是一个非常重要也是一个非常容易弄错的方面,我们将在下一章单独讨论。
有了上一章的铺垫,本章类比不是特别多了,如果有概念忘记的(特别在讲模块化的时候)一定要回到上一章看看,不然下一章一定会更加痛苦。