跳至主要內容

Dagger 2 从 0 到 1 之旅

JI,XIAOYONG...大约 8 分钟dagger2

前言

Dagger 2是 Google 维护的一款可用于JavaAndroid的依赖注入框架。

本文主要是简单梳理Dagger 2中各个注解的作用,以及其简单用法,不涉及具体项目应用。

先解释几个概念:

  • 依赖注入:是一个对象(或静态方法)给另一个对象提供依赖的技术。

  • **依赖**是可以使用的对象(Service),而把依赖提供给使用该依赖的对象(Client)的过程叫做注入

例如,下面这段代码中Service就是Client的依赖。:

class Service

class Client(){
    init {
        val service = Service()
    }
}

但是如果每个依赖都这样写的话,如果Service类的构造方法有变更,就需要同时也更改Client对应的方法,这样深耦合的代码显然不是我们需要的。

Dagger 2 就是为了帮助我们解决这个问题,在使用它之后,Client类的代码只需要这样写成类似下面这样(示例代码):

class Client{

    lateinit var service: Service

    init {
        //TODO 某个将 Service 依赖注入的方法 magicFun()
        val newService = service//使用 Service 的实例 service
    }
}

可以看到,这时Service的实例化过程被移到了Client的外部某处,这样如果Service构造方法有更新时,我们只需要统一去修改magicFun()中对应的代码即可。

那么这一切Dagger 2到底是如何实现的呢?

Dagger 2 具体实现

@Inject

首先需要请出第一个主角——@Inject

Dagger 2中,@Inject主要做两件事 ❶ 标记依赖类的构造方法;❷ 标记需要框架自动实例化的对象:

class Service @Inject constructor()//❶标记依赖类的构造方法

class Client{

    @Inject
    lateinit var service: Service//❷标记需要框架自动实例化的对象
	...
}

这样Dagger 2 就知道了有个对象需要它来帮助我们注入,同时也知道了有一个构造方法来实例化Service对象。但这时如何将二者联系起来呢?

@Component

这就要提到第二个主角——@Component

@Component标记的类是将一个类和他的依赖联系在一起的桥梁,通常是一个抽象类或者接口

@Component
interface ClientComponent{
    fun inject(client: Client)
}

至此,ClientService通过ClientComponent联系在一起,在使用时只需要将Client的引用传入即可:

    init {
        //方式❶ DaggerClientComponent.create().inject(this)
        //方式❷ DaggerClientComponent.builder().build().inject(this)
        val newService = service
    }

以上完整的代码可以参考这里,若无法显示可点击这里查看open in new window

到目前为止,对于我们自己定义的类,我们只需要使用@Inject标记其构造方法,然后再在使用该类的时候使用@Inject标记该对象,在需要使用该对象的地方通过@Component类传入使用该依赖的类的引用即可。

但是很显然实际开发中,不是所有的Service类都可以被我们随意修改,如果Service类是第三方提供的类,显然我们是无法用@Inject修饰其构造函数的。

@Module 和@Provides

为了解决第三方依赖的问题,我们要引入另外两个主角——@Module@Provides

@Provides用来提供一个方法,我们可以在其内部实例化并返回Service类,这样子当用到Service的时候,@Component类只需要找到@Provides提供的这个方法,并获取到他实例化好的Service对象注入到Client中就可以了。

@Module则是提供一个(注意是类,而非接口),像一个袋子一样把@Provides提供的方法“装”到一起,打包提供给@Component类。

@Module
class ClientModule{

    @Provides
    fun getService() = Service()
}

@Component(modules = [ClientModule::class])//Component 可以有多个 Module 类
interface ClientComponent{
    fun inject(client: Client)
}

上述代码中的@Component(modules = [ClientModule::class])将装有可以产生依赖的@Provides方法的“大袋子”@Module和“桥梁”@Component关联到了一起。

@Component在产生依赖的时候会先到@Module类中的@Provides方法中查找;如果找不到才会再到@Inject中查找。(也就是说,此时Service类的@Inject构造方法其实是失效了的,完全可以没有@Inject注解——第三方类即是如此)。

上述完整代码如下,若无法显示可点击这里查看open in new window

解决了第三方依赖引用的问题,还有一个非常重要的问题——我们使用的绝大多数类肯定不止一个构造方法,那么假设依赖类Service现在有两个构造方法,我们需要分别这两个构造方法,这种情况又该怎么处理呢?

class Service @Inject constructor(var string: String = "default")

很明显,这时候@Inject注解已经没用了,一个类只能有一个构造方法被@Inject修饰,否则会报错:错误: Types may only contain one @Inject constructor

去掉@InjectService类变成如下:

class Service(var string: String = "default")

尝试在@Module中添加另外一个@Provides方法使用另外一个带参构造函数:

@Module
class ClientModule{

    @Provides
    fun getService() = Service()

    @Provides
    fun getServiceWithArgs() = Service("Args")
}

运行时发现会出错,因为有两个方法都可以提供Service@Component产生了迷失,不知道用哪一个好,导致错误。

@Named 和@Qualifier

为了解决多个构造函数导致的问题,这时就需要第五个主角**@Named以及幕后英雄@Qualifier**

首先,上述问题的解决方案是在另外一个方法上加一个注解@Named,表示他是一个特殊的方法:

    @Provides @Named("Args")
    fun getServiceWithArgs() = Service("Args")

当在Client中想使用这个方法的依赖时:

    //@field:是 kotlin 中注解字段特别需要的,在 Java 中可以直接写成@Named("Args")
    @Inject
    @field:Named("Args")
    lateinit var service: Service

查看@Named源码:

@Qualifier
@Documented
@Retention(RUNTIME)
public @interface Named {

    /** The name. */
    String value() default "";
}

发现@Qualifier才是他实现标识限定符注解(Identifies qualifier annotations)的力量之源。查看@Qualifier注解可以知道,我们也可以自定义基于@Qualifier的注解来实现和@Named完全一致的功能。

@Qualifier
@MustBeDocumented
@kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
annotation class YourQualifierName(//YourQualifierName 可以是任意你喜欢的名字
    /** The name.  */
    val value: String = ""
)

之后我们就可以使用@YourQualifierName替代@Named实现标识不同注解的作用,从而支持有多个构造函数的Service类的初始化。

上述完整代码如下,若无法显示可点击这里查看open in new window::

@Component可以有多个@Module,他们之间的关系可以用下图表示:

@Singleton 和@Scope

在实际开发中,我们需要有的类只能有一个实例,从而在不同的地方共享一些数据——即单例,这种情况就需要另外一个角色@Singleton和他的幕后英雄@Scope

@Singleton 是用来标记类在其范围内只能被实例化一次。

@Scope
@Documented
@Retention(RUNTIME)
public @interface Singleton {}

通过查看其源码可以知道其背后是@Scope在起作用,@Scope的作用是限定其修饰的类的范围,适用于有可注入的构造函数并且包含控制类型实例如何重用的类。没有@Scope修饰的实例在构造完毕后就会失去控制,不再关心后续的发展(then forgets it),而@Scope修饰的类会在实例构造完毕后,继续保留一遍下一次可能的复用,当有多个线程可以访问该实例时,他的实现应该是线程安全的(it‘s implementation should be thread safe)。

此外@Component应该和他所包含的@Module@Provides@Scope范围一致:

@Singleton
@Component(modules = [ClientModule::class])
interface ClientComponent{
    fun inject(client: Client)
}
@Module
class ClientModule{

    @Provides
    fun getService() = Service()

    @Singleton
    @Provides @Choose("Args")
    fun getServiceWithArgs() = Service("Args")
}

此外两个关系为**dependencies**的@Component可以分别拥有相同名称的@Inject@Module@Provides而不会被merge,两者可以相互访问。

而**subcomponents**则不能和@Component有以上相同的项。

Subcomponent从它的父类访问所有依赖

@Component只能访问在基类@Component接口暴露的公共性的依赖

——Subcomponents 和 Component Dependencies——Sinyuk Blogopen in new window

他们之间的关系可以表示为下图:

参考资料

android-cn:依赖注入—— Githubopen in new window

Dependency Injection ——wikipediaopen in new window

Elye 的 Dagger 2 系列open in new window

Dagger 2 官方手册open in new window

Android - Dagger2 使用详解——简书open in new window

Subcomponents 和 Component Dependencies——Sinyuk Blogopen in new window

文章标题:《Dagger 2 从 0 到 1 之旅》
本文作者: JI,XIAOYONG
发布时间: 2019/01/26 15:16:17 UTC+8
更新时间: 2023/12/30 16:17:02 UTC+8
written by human, not by AI
本文地址: https://jixiaoyong.github.io/blog/posts/2822c354.html
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 许可协议。转载请注明出处!
你认为这篇文章怎么样?
  • 0
  • 0
  • 0
  • 0
  • 0
  • 0
评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v2.15.8