Dagger 2 从 0 到 1 之旅
前言
Dagger 2是 Google 维护的一款可用于Java和Android的依赖注入框架。
本文主要是简单梳理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)
}至此,Client和Service通过ClientComponent联系在一起,在使用时只需要将Client的引用传入即可:
init {
//方式❶ DaggerClientComponent.create().inject(this)
//方式❷ DaggerClientComponent.builder().build().inject(this)
val newService = service
}以上完整的代码可以参考这里,若无法显示可点击这里查看:
到目前为止,对于我们自己定义的类,我们只需要使用@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注解——第三方类即是如此)。
上述完整代码如下,若无法显示可点击这里查看:
解决了第三方依赖引用的问题,还有一个非常重要的问题——我们使用的绝大多数类肯定不止一个构造方法,那么假设依赖类Service现在有两个构造方法,我们需要分别这两个构造方法,这种情况又该怎么处理呢?
class Service @Inject constructor(var string: String = "default")很明显,这时候@Inject注解已经没用了,一个类只能有一个构造方法被@Inject修饰,否则会报错:错误: Types may only contain one @Inject constructor。
去掉@Inject后Service类变成如下:
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类的初始化。
上述完整代码如下,若无法显示可点击这里查看::
@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接口暴露的公共性的依赖
他们之间的关系可以表示为下图:
