Dagger2 in Android(二)进阶

前面已经讲了 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 还可以管理对象的生命周期,这是一个非常重要也是一个非常容易弄错的方面,我们将在下一章单独讨论。

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,923评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,154评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,775评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,960评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,976评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,972评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,893评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,709评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,159评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,400评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,552评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,265评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,876评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,528评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,701评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,552评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,451评论 2 352

推荐阅读更多精彩内容