参考:
Dagger2系统学习
Dagger2 入门,以初学者角度.
dagger 2 详解
在开始之前先扯会儿淡记录一下,最近又换了个公司(上个公司裁人),不过嘛,也不怎么样。所以今年必须要好好加油,努力在限定时间内(最多一年)进入更优秀的公司。说实话,人在全力投入的时候,就算很短的时间,也能有飞快的进步。
扯淡结束,谈谈为什么要学习dagger2.因为公司从外包那里买了代码,要我们在它的基础上继续做二次开发。我原以为外包的代码质量会很差,但是当我一打开他们的代码,发现他们的技术选型都是最新的,mvp + retrofit + rxjava2 + dagger2, 着实惊讶了一阵。不过苦恼也来了,就是dagger2是我以前从来没接触过的框架,据说上手难度还不小,有很多人就直接从入门到放弃了。但是对于我来说,有了梦想,还会害怕小小的困难?而且在高质量的别人的代码上继续开发正是我求之不得呢,难道在又老又旧的代码上继续开发更好?
添加依赖
dagger2在github上的地址是这个:
https://github.com/google/dagger
新建一个项目,我们按照地址中的说明添加依赖
1 | dependencies { |
因为最新版本是2.16,所以就添加2.16的版本了。
看到后面还有一个叫dagger.android的库可以添加,但是我们这里暂时不添加,那个是dagger2专门针对Android的库,在后续的文章中再介绍。
@Inject
先来说@Inject这个注解。
首先创建一个User的实体类, 然后在构造方法上添加@Inject注解
1 | public class User { |
Activity中依赖了mUser这个成员变量,给mUser添加@Inject注解
1 | public class MainActivity extends AppCompatActivity { |
然后创建Component类。
创建MainComponent类,专门对应于MainActivity
1 |
|
然后选择菜单build->Make Project,可以看到在build/generated/source/apt/debug/包名/目录下有一些文件生成
然后这个DaggerMainComponent就可以为我们所用了。
1 |
|
打印结果:
1 | 05-11 16:19:54.556 25822-25822/me.mundane.dagger2learning D/MainActivity: mUser = me.mundane.dagger2learning.bean.User@65fb450 |
可以看到这个mUser对象已经不再为null了,那么它是什么时候被赋值的呢?答案就在DaggerMainComponent.builder().build().inject(this);
这句代码里。
DaggerMainComponent.java
1 |
|
MainActivity_MembersInjector.java
1 | public static void injectMUser(MainActivity instance, User mUser) { |
另外值得注意的一点是,使用@Inject时,不能用private修饰类的成员变量,否则就会报如下的错误,注意红色箭头的地方:“Dagger does not support injection into private fields”
component的另一种写法
刚才我们看到component是这样一种写法:
1 |
|
但其实也可以这样,我们新建一个MainComponent2的类
1 |
|
然后改写MainActivity中注入代码的方式
1 |
|
打印的结果:
1 | 05-11 16:31:59.028 26504-26504/me.mundane.dagger2learning D/MainActivity: mUser = me.mundane.dagger2learning.bean.User@e352b4e |
可以看到User对象也被成功注入了。
但是一般来说,我们都使用第一种写法,避免自己一个一个手动赋值。
@Module和@Provides
之前我们能成功注入一个User对象是因为我们在User的构造方法上添加了@Inject的注解,现在我们想注入一个textView对象,很明显我们无法做到在textView的构造方法上添加@Inject的注解(无法修改Android的系统源码),所以就有了@Module和@Provides这一种方式。
首先新建一个TextViewModule的类
1 |
|
provideTextView方法需要一个Context参数,这个context我们从外部传入,然后添加一个provideContext的方法, TextViewModule的完整代码如下
1 |
|
接着我们还要修改Compoment和Activity中的代码。
首先修改MainComponent:
1 | (modules = TextViewModule.class) |
然后再次Make Project(记得每次新增或者修改Component或者新增@Inject的对象之后Make Project)
然后,修改Activity中的代码,传入一个component的构建过程中传入一个textViewModule对象,完整代码如下:
1 | public class MainActivity extends AppCompatActivity { |
打印结果:
1 | 05-11 17:36:54.640 30446-30446/me.mundane.dagger2learning D/MainActivity: mTv = android.widget.TextView{5905649 V.ED..... ......ID 0,0-0,0} |
可见这个textView对象已经被成功注入了。
由此可见,Dagger2框架调用provideTextView方法获取一个TextView实例时,发现要传一个Context类型的参数,这时候它会查找被@Provides注解并且返回值为Context类型的方法获取一个Context实例传入provideTextView方法,如果找不到,则会查找Inject注解的构造函数,看构造函数是否存在参数。
现在再来看,我们还可以把TextViewModule简化一下
1 |
|
两种写法都可以。
@Singleton
@Singleton是@Scope中的一种,@Scope表示注入对象的作用域或者说生命周期。@Singleton的字面含义是单例,但是它实际上并没有这么简单。我们还是实际来使用一下看看这个@Singleton
的效果吧。
新建一个HttpModule类来提供OkhttpClient的实例
1 |
|
需要在提供OkhttpClient实例的这个方法上添加@Singleton
注解。
然后新建一个SingletonComponent
1 |
|
module的provide方法使用了@Singleton或者别的Scope ,那么 component也必须使用@Singleton或者相同的Scope
然后在Activity中
1 | public class SingletonActivity extends AppCompatActivity { |
打印的结果:
1 | 05-14 10:03:49.701 29536-29536/me.mundane.dagger2learning D/SingletonActivity: mOkHttpClient1 = okhttp3.OkHttpClient@bc66074 |
可以发现,这两个实例是一样的,这就是@Singleton所表示的单例的意思了。
但是事情并不是这么简单。
现在我们再新建一个页面,叫做SingletonActivity2
1 | public class SingletonActivity2 extends AppCompatActivity { |
再新建一个SingletonComponent2,代码如下:
1 |
|
依然是HttpModule提供依赖
然后我们修改SingltonActivity中的代码,提供一个按钮,点击跳转到SingletonActivity2
1 | public void go2SingletonActivity2(View view) { |
接着我们看看这两个Activity中的打印结果
1 | 05-14 10:58:04.956 9029-9029/me.mundane.dagger2learning D/SingletonActivity: mOkHttpClient1 = okhttp3.OkHttpClient@c043299 |
可以发现,在同一个页面中,是两个相同的实例,但是在不同的页面中,是不同的实例。
解释:@Singleton的生命周期依附于Component,不同的Component就算关联了相同的Module,创建的实例也不同。
自定义@Scope和dependencies
上面我们说到,在一个项目中,多个页面的okhttpClient不是同一个实例,而如果我们希望提供的OkHttpClient是一个实例,该怎么办呢?
首先,创建一个Module,提供OkhttpClient
1 |
|
然后创建一个全局的Component
1 |
|
module的provide 方法使用了 scope(这里使用了@Singleton),那么component就必须使用同一个注解因此在AppComponent上也加入了@Singleton注解。这里建立全局的目的很明确,就是提供了单例OkHttpClient。
然后就在其他的Component中引用全局的Component, 这里就需要用到@Component中的dependencies属性了。
SingletonComponent.java
1 |
|
SingletonComponent2.java
1 |
|
然后我们在HttpModule中不再需要提供创建OkHttpClient实例的 provide 方法了。我们把代码注释掉。
1 |
|
Make Project一下, 然后发现报错了
报错的原因说的是Singleton的组件不能依赖其他的scope的组件。其实这里就涉及到这样一条规律:Singleton的组件不能依赖其他的scope的组件,只能其他scope的组件依赖Singleton的组件。
所以我们将SingletonComponent和SingletonComponent2上面的@Singleton注解去掉,再次Make Project, 发现又报错了
报错原因说没有Scope注解的的组件不能依赖有Scope注解的组件。好吧,那我们参照一下@Singleton然后自己写一个自定义的Scoped注解吧。
首先打开@Singleton这个注解的源码:
1 | /** |
可以发现@Singleton注解也是一个@Scope注解,所以我们自定义一个ActivityScope,表示这个Activity页面中的局部单例
1 |
|
然后我们修改HttpModule
1 |
|
Person类是我自己自己定义的一个实体类,很简单的。
再修改SingletonComponent和SingletonComponent2
1 |
|
1 |
|
再修改SingletonActivity的代码
可以看到DaggerSingletonComponent中多了一个appComponent的方法,需要传入一个AppComponent的实例。SingletonComponent和SingletonComponent2既然依赖AppComponent,肯定需要你传入一个AppComponent实例,而且需要同一个AppComponent的实例,因为之前说过了, 不同的Component就算关联了相同的Module,创建的实例也不同。
所以我们将这个AppComponent实例的创建放在Application创建的时候,代码如下:
1 | public class MyApplication extends Application { |
抽取一个BaseActivity
1 | public class BaseActivity extends AppCompatActivity { |
再修改SingletonActivity, SingletonActivity2的修改也是类似
1 | public class SingletonActivity extends BaseActivity { |
查看日志打印:
SingletonActivity:
1 | 05-14 17:27:33.463 9874-9874/me.mundane.dagger2learning D/SingletonActivity: mOkHttpClient1 = okhttp3.OkHttpClient@829f409 |
SingletonActivity2:
1 | 05-14 17:27:40.882 9874-9874/me.mundane.dagger2learning D/SingletonActivity2: mOkHttpClient1 = okhttp3.OkHttpClient@829f409 |
可以发现,4个OkhttpClient实例都是同一个,说明确实是应用内的单例。
同一个页面中的Person实例是同一个,但是不同页面中的Person就不是同一个了,实现的是页面内的单例。
总结起来就是,Scope的生命周期依附于component,不同的component,创建的实例不同。
@SubComponent
@SubComponent如下:
首先写一个ChildComponent
1 |
|
看一下这个ChildModule
1 |
|
可以看到这里要生成OkhttpClientManager的实例需要一个okHttpClient的实例,本应该在ChildMudule里面继续提供创建OkHttpClient实例的方法,但是这里就不一样了,他会主动到“父类”FatherComponent与之关联的OkhttpClientModule里面去找这个实例,那么怎么将父子组件进行关联呢?
我们看FatherComponent
1 |
|
这样就完成了父子组件之间的关联。
然后再Activity中使用
1 | public class SubcomponentActivity extends AppCompatActivity { |
打印结果:
1 | 05-15 10:31:39.408 25998-25998/me.mundane.dagger2learning D/SubcomponentActivity: mOkhttpClientManager = me.mundane.dagger2learning.bean.OkhttpClientManager@66c6981 |
可以看到,成功注入了一个OkhttpClientManager对象。
简单总结一下@Subcomponent的使用:
- 子组件的声明方式为@Subcomponent
- 在父组件中要声明一个返回值为子组件的方法
- 上面第2条中的方法可以传递参数,当子组件需要什么Module时,就在该方法中添加该类型的参数,但是参数类型必须是子组件关联的modules中的一种,否则会报错
注意:
- 注意:用@Subcomponent注解声明的Component是无法单独使用的,想要获取该Component实例必须经过其父组件
- 父亲和孩子Component都具备Scope,但是父亲定义的Scope大于孩子定义的Scope。父亲FatherComponent 定义的Scope是@Singleton,而孩子SubComponent定义的Scope也是@Singleton,这样就会报错。那么修改孩子的Scope的级别,例如修改为自定义的@ActivityScope是可以的。