Dagger2 in Android(二)进阶

1,271 阅读7分钟

系列文章

前面已经讲了 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

QualifierNamed 的作用一模一样。只不过 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 提供对象;后者作用仅仅是创建对象。

包含关系(子组件)(组件继承)

依赖就像朋友,对方愿意才可以分享。包含就像父母,分享是无条件的。

声明继承需要以下几步

  1. 子 Component 用 @Subcomponent 注解。
  2. 子 Component 声明一个 Builder 来告诉父 Component 如何创建自己。
  3. 父 Component 对应的 Module 用 subcomponents 属性来指明拥有哪些子 Component。
  4. 父 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 还可以管理对象的生命周期,这是一个非常重要也是一个非常容易弄错的方面,我们将在下一章单独讨论。

有了上一章的铺垫,本章类比不是特别多了,如果有概念忘记的(特别在讲模块化的时候)一定要回到上一章看看,不然下一章一定会更加痛苦。