Dagger2是一个依赖注入(DI)框架。那么什么是依赖注入?其实我们一直在使用依赖注入,只是没有提到这个概念。例如下面最简单的例子:
class Car{
Engine engine;
public User(){
engine=new Engine();
}
public setEngine(engine){
this.engine=engine;
}
}
Engine engine对象的来源要么是自己在构造函数里面new出来,要么是通过setEngine()从外部传入。这两种方式都是依赖注入。都是需要我们手动new对象的。 而我们要讨论的是另外一种依赖注入方式,就是要把new对象的操作统一放在一个地方一起操作,而不是在业务代码里面到处new对象。这种方式有个专门的名称叫做控制反转(IOC),控制反转的意思就是new对象不是由用户亲自操作,而是由专门的机制替用户来完成。提供这种机制的框架我们称为DI框架或者IOC框架,IOC和DI的概念本质上就是对提供这种机制的框架的不同的描述方式罢了,有时两者概念可以通用,最典型的就是Java后端用的Spring IOC框架(官方是这样叫的,如果你愿意叫Spring DI框架也是没什么问题的)。而在Android中,就是我们的Dagger框架。
Dagger历史
Dagger1最初是由square公司开发的,google要推出依赖注入框架,发现square公司已经开发出来了,于是接手了Dagger1,并改了名字叫Dagger2。依赖注入框架有两者实现方式,一种是通过反射在运行时注入,或者叫动态注入,Spring IOC就是使用这种方式实现。另一种就是在编译时通过APT实现,或者叫静态注入。Dagger1是静态动态混合开发的,Dagger2是通过静态方式开发的。Dagger的本意是DAG(有向无环图)的意思,有向无环图就是指对象之间的依赖关系。加上后缀就变成了dagger,正好是一个英文单词。而dagger这个单词就是匕首的意思。但这把匕首并不是特别好用,主要原因是dagger资料太少,官方文档举了几个Coffe的例子,并没有什么实战意义,而国内能搜索到的dagger博客很多也是一知半解,而且对于android开发来说依赖注入的概念还是比较陌生的,还有一个主要的原因是用起来也比较麻烦。 难的原因: 1.中文资料少 2.依赖注入概念比较陌生 3.框架本身用起来麻烦 4.使用不当(不易察觉)造成非常奇怪的问题
单独一个原因还好,多个加在一起就非常的头疼。但Dagger框架本身是没什么问题的,也没什么bug。为了降低使用难度,google推出了Hilt,Hilt是基于Dagger的,并不是一个全新的框架。Hilt也英文意思是刀柄,意思是Dagger这个匕首的刀柄并不好用,给你换个。 我缺的是刀柄吗?你官方多写点和Android相关的实战例子会死? 既然是基于Dagger那么本质上你还是要学Dagger的,而且理解Dagger本身的概念才是核心,重点还是在Dagger,Hilt只是语法糖。
1.基本例子
下面我们使用Dagger来替我们new对象。 第一步:我们在User的构造方法上面添加一个@Inject注解,这是告诉Dagger,User这个类你帮我创建。
import javax.inject.Inject;
public class User {
@Inject
public User() {
}
}
第二步:我们在MainActivity声明我们的User user;并且添加@Inject注解。
public class MainActivity extends AppCompatActivity {
@Inject
User user;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
这里可能就有个比较然后迷惑的地方,为什么这个@Inject可以在构造函数上又可以在常用变量上?因为放在不同位置,这个注解的作用是不一样的。我们看一下他的实现:
@Target({ METHOD, CONSTRUCTOR, FIELD })
@Retention(RUNTIME)
@Documented
public @interface Inject {}
这个注解可以放在方法,构造器和成员变量上,如果放在构造器,表示这个类需要由 Dagger替我们创建,如果放在成员变量上,表示这个成员变量的对象由Dagger为我们提供,类似于生产者消费者的关系,而Dagger只是个生产对象的工厂。
第三步:注入依赖。经过前面的两步,我们已经有了变量声明User user;和Dagger为我们new出来的User()对象,但是这两者还没有关联起来,现在就是要将他们关联起来。我们需要用到Dagger提供的组件将他们关联起来。创建接口并且添加@Component注解,接口名字是可以随便取的。这里的inject方法名字是可以随便取的,里面的MainActivity表示在哪里完成注入。这里可能就非常令人困惑了,这到底是要干什么?
@Component
public interface AppComponent {
void inject(MainActivity mainActivity);
}
下面我们要Build或者Rebuild一下工程,就会真相大白了。 我们可以看到在build目录下面多出了三个文件,DaggerAppComponent,User_Factory和MainActivity_MembersInjector。这三个类都是Dagger通过APT帮我们生成的,User_Factory非常明显就是用来创建User对象的,DaggerAppComponent就是我们上面写的AppComponent的实现类,而MainActivity_MembersInjector只是个中间的工具类。 来看下面的DaggerAppComponent一个关键代码你就明白了,这个方法将new出来的User和MainActivity关联上了。 大致原理就是这样,三个类的源码也比较简单,打开看一下就明白了。总之,通过这些生成类就可以实现关联了。
@CanIgnoreReturnValue
private MainActivity injectMainActivity(MainActivity instance) {
MainActivity_MembersInjector.injectUser(instance, new User());
return instance;
}
第四步:调用执行关联代码。代码生成了,但是必须调用才能执行啊(这步其实框架本来可以替我们完成,但dagger没有,后面Hilt帮我们完成了)。
public class MainActivity extends AppCompatActivity {
@Inject
User user;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
DaggerAppComponent.create().inject(this);
Log.d("MainActivity", "user:" + user);
}
这时候运行代码,我们发现User对象已经自动为我们注入了。
D/MainActivity: user:com.xzh.androidbase.mvp.model.entry.User@3571984
这里可能有同学就问了,我就是简单的new一个User对象,直接new不就完了吗?有必要这么麻烦吗?这就要聊聊Dagger到底解决了什么痛点,或者说,他到底有什么优势。 1.解耦。你写的一个构造方法,但是有一天这个构造方法改了,那么所有用到的地方都要修改。什么?通过重构工具也可以?是不可以的,因为多或者少参数都是不能重构的。 2.单例。通过Dagger提供的单例功能,可以自动生成单例,而不要自己写单例。什么?我可以自己写单例,而且有工具可以自动生成单例?那你可以写局部单例吗,只在单个Activity生效?这些Dagger都可以帮你做到,不用自己实现单例。 3.测试。是的,Dagger结合测试是非常方便的,因为测试需要不停的mock对象,而Dagger正好就是提供对象的。什么?我从来不写测试,而且没时间写?那你等着出bug慢慢找bug吧! 4.封装和简洁性。通过将new对象这件事情封装起来,不会看到到处凌乱的new对象代码,项目将变得非常简洁。 5.方便管理。对象的创建方式可能发生修改,可能要到处修改创建的地方,找起来比较麻烦,用Dagger后就可以在一个地方统一管理。
我觉得单单测试这一个优点就有足够的理由使用Dagger。就当作是用Dagger来给测试做mock对象的准备吧。
2.项目实际例子
上面的User例子只是个玩具例子,在实际项目中是没什么意义。而且有时候对象并不是我们自己写的,而是第三方框架里面的,那样的话我们就不能在构造函数上面使用@Inject注解了。这时候我们需要使用另一种方式来注入依赖
2.1使用Module注入需要依赖的模块
在项目中我们经常需要用到网络,Retrofit,Okhttp等,这些都是第三方提供的,我们不能在他们的类的构造器上用@Inject注解,但非要这样的话,就只能继承那个类,这样就违背了我们的初衷,将代码变得更难以维护了。我们使用的新方式是用Dagger给我们提供的Module机制,我们看下面的代码。 我们定义一个类并给他添加@Module注解,写一个方法返回Retrofit对象,并在这个方法上面添加@Provides注解,表示这个是需要注入这个返回的对象,相对于@Inject注解,只是他是用在@Module里面了。
@Module
public class NetModule {
@Provides
public Retrofit provideRetrofit(){
return new Retrofit
.Builder()
.baseUrl("www.google.com")
.build();
}
}
在Component里面指定我们刚刚写的NetModule,可以指定多个Module。这样我们就已经可以提供依赖注入的方式获取Retrofit对象了
@Component(modules = {NetModule.class})
public interface AppComponent {
void inject(MainActivity mainActivity);
}
老样子,我们现在直接用@Inject把Retrofit变量注入就可以用了。
public class MainActivity extends AppCompatActivity {
@Inject
Retrofit retrofit;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
DaggerAppComponent.create().inject(this);
Log.d("MainActivity", "retrofit:" + retrofit);
}
输出结果。
D/MainActivity: retrofit:retrofit2.Retrofit@c8c02a7
现在对原来的代码做一些改动,provideRetrofit方法多了一个OkHttpClient clint的参数,这个参数的内容可以通过上面的provideOkHttpClient方法获取。这是Dagger提供的一种机制,参数真实对象可以提供别的被@Provides注解的方法注入。
@Module
public class NetModule {
@Provides
public OkHttpClient providekHttpClient(){
return new OkHttpClient.Builder()
.callTimeout(10, TimeUnit.SECONDS)
.build();
}
@Provides
public Retrofit provideRetrofit(OkHttpClient client ){
return new Retrofit
.Builder()
.baseUrl("https://www.google.com")
.client(client)
.build();
}
}
更进一步我们可以获取到网络请求的Service类。到这里,已经能够应付大部分的开发需求了。
@Provides
public RepoService provideRepoService(Retrofit retrofit){
return retrofit.create(RepoService.class);
}
到目前为止,用Module机制注入依赖就讲完了。下面我们进一步思考一些细节问题。 Retrofit这个对象每次获取到的对象都是不一样的,而且Retrofit这个对象是非常复杂的,里面的变量非常的多,也就是说,创建Retrofit对象是非常的销毁资源的。所以什么样什么办法将Retrofit对象设为单例?既然我们已经用的Dagger,那么当然是用Dagger的方式来实现单例。
2.2利用作用域实现单例
先看一下用法,后面解释为什么要这样使用。 在每个Provides上面加上@Singleton注解
@Module
public class NetModule {
@Singleton
@Provides
public OkHttpClient providekHttpClient(){
return new OkHttpClient.Builder()
.callTimeout(10, TimeUnit.SECONDS)
.build();
}
@Singleton
@Provides
public Retrofit provideRetrofit(OkHttpClient client ){
return new Retrofit
.Builder()
.baseUrl("https://www.google.com")
.client(client)
.build();
}
@Singleton
@Provides
public RepoService provideRepoService(Retrofit retrofit){
return retrofit.create(RepoService.class);
}
}
在AppComponent上也加上@Singleton。这样就已经实现单例的,使用起来还是比较方便的。
@Singleton
@Component(modules = {NetModule.class})
public interface AppComponent {
void inject(MainActivity mainActivity);
}
下面测试一下。我们测试RepoService这个对象。
public class MainActivity extends AppCompatActivity {
@Inject
RepoService repoService;
@Inject
RepoService repoService2;
@Inject
RepoService repoService3;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
DaggerAppComponent.create().inject(this);
Log.d("MainActivity", "repoService1:" + repoService);
Log.d("MainActivity", "repoService2:" + repoService2);
Log.d("MainActivity", "repoService3:" + repoService3);
}
}
可以发现三个对象都是一样的。
D/MainActivity: repoService1:retrofit2.Retrofit$1@5a3cfde
D/MainActivity: repoService2:retrofit2.Retrofit$1@5a3cfde
D/MainActivity: repoService3:retrofit2.Retrofit$1@5a3cfde
但如果你在另外一个Activity或者Fragment注入这个RepoService的对象的话,你会发现是另外一个对象。这就说明这个单例并不是全局单例,而仅仅是在某个Activity内才有效的局部单例,这显然是不满足要求的。
D/SecondActivity: repoService1:retrofit2.Retrofit$1@7f6cd63
D/SecondActivity: repoService2:retrofit2.Retrofit$1@7f6cd63
D/SecondActivity: repoService3:retrofit2.Retrofit$1@7f6cd63
那么到底是怎么回事,是Dagger提供的这个@Singleton有问题吗?Dagger的设计者应该也没这么傻。这里就要说说Scope的概念。
2.3 Dagger的注入流程分析
这点是重中之重,没弄明白这个后面将非常难懂。我们来回顾一下下面这个inject方法。
@Singleton
@Component(modules = {NetModule.class })
public interface AppComponent {
void inject(MainActivity mainActivity);
}
这个方法是我们自己定义的,名字其实可以随便取,这个方法的关键点是他是一个void方法,这是有特别的含义的,他的本质其实是一个set方法。什么?set方法?是的,没有开玩笑,虽然非常奇怪,但他就是。他的本质就是类似下面的代码,将Module里面的provider的对象赋值给MainActivity的成员变量,就是在这里依赖注入的。所以一般这个方法取名字叫inject,你也可以取名字叫setValue。
public void inject(MainActivity mainActivity){
mainActivity.成员1=module.对应的provider;
mainActivity.成员2=module.对应的provider;
}
你这么知道是这样的?因为源码就是这样实现的,其实这个inject方法才是Dagger的精髓和切入口。我们来看下源码就知道了。下面我们把inject这个方法名改为setValue,因为在源码里面可能引起误解。
```java
@Singleton
@Component(modules = {NetModule.class })
public interface AppComponent {
void setValue(MainActivity mainActivity)
}
public final class DaggerAppComponent implements AppComponent {
@Override
public void setValue(MainActivity mainActivity) {
injectMainActivity(mainActivity);}
private MainActivity injectMainActivity(MainActivity instance) {
MainActivity_MembersInjector.injectUser(instance, provideUserProvider.get());
MainActivity_MembersInjector.injectRetrofit(instance, provideRetrofitProvider.get());
MainActivity_MembersInjector.injectClient(instance, providekHttpClientProvider.get());
MainActivity_MembersInjector.injectRepoService(instance, provideRepoServiceProvider.get());
MainActivity_MembersInjector.injectRepoService2(instance, provideRepoServiceProvider.get());
MainActivity_MembersInjector.injectRepoService3(instance, provideRepoServiceProvider.get());
return instance;
}
}
我们看到setValue方法,也就是原来的inject方法调用injectMainActivity(MainActivity instance)方法,而这个方法调用 MainActivity_MembersInjector.injectXXX方法。这里的injectXXX方法名是Dagger取的,前面方法名换成setValue就是为了说明这个injectXXX名字不是我们定义的。(小问题,源码看一下就明白了,取名setValue也更能说明这个方法的含义) 以Retrofit为例。 MainActivity_MembersInjector.injectRetrofit(instance, provideRetrofitProvider.get()); 我们看看injectRetrofit的实现,如下面的代码,看参数就明白了,就是将Module的Retrofit对象赋值给MainActivity的Retrofit对象,这样就完成了依赖注入(也就是赋值)了。
@InjectedFieldSignature("com.xzh.androidbase.mvp.ui.activity.MainActivity.retrofit")
public static void injectRetrofit(MainActivity instance, Retrofit retrofit) {
instance.retrofit = retrofit;
}
到这里,注入流程就讲完了,其实也不难,只是这个void inject(MainActivity mainActivity)方法太奇怪了,不解释谁知道是这个含义?在接口里面定义一个这种方法让新手摸不着头脑。其实这个设计是非常糟糕的,有点暗示的味道,暗示你妹啊,谁知道你什么意思?按理说,应该在这个方法上面设计一个类似@setter注解来标记这个方法,表明这个方法是用来赋值的,但是这样又容易和@Inject注解混淆,因为@Inject也有赋值的含义,而且又不能用@Inject来标记方法,这样做的话,@Inject注解用法就很混乱。取别的名字也不太好,干脆不取了,就默认叫void inject来暗示这是赋值方法。其实这个名字强制写为setValue比较好,并且强制不能取别的名字,要是能随便改名字就没有暗示的味道了。叫inject容易和@Inject搞混,新手会有这样的疑惑,怎么别的地方@Inject这里又inject?也不知道是谁想取的inject说这么多,就是为了能弄明白这个方法的真正含义,有点罗嗦了。
2.4 Scope
作用域(Scope)的定义:将某个对象的生命周期限定为其组件的生命周期。 前面的@Singleton只能实现局部单例的效果,那么是为什么呢?关键点就在于DaggerAppComponent create的时间,在前面我们都是在Activity里面create的,要想变成全局的,只需要在Application里面创建这个DaggerAppComponent就可以了。
public class App extends Application {
static AppComponent appComponent= DaggerAppComponent.create();
public static AppComponent getAppComponent() {
return appComponent;
}
}
然后在Actvity里面调用:这样就可以实现全局单例了。为什么这样就是全局单例?这里就需要理解前面小结的内容了。inject(this)就是给MainActivity里面被@Inject标记的成员变量赋值,那么对象从哪里来?从App.getAppComponent()这里来的,也就是DaggerAppComponent对象,DaggerAppComponent再get对应的provider对象,而DaggerAppComponent对象现在是App里面成员变量,也就是个全局变量,并且是static的,所以可以实现全局单例的效果。这只能说明全局。要想说明单例是要看Dagger这么实现Scope。
App.getAppComponent().inject(this);
2.5组件依赖
@Component有两个成员变量,modules前面已经用过了,现在我们需要使用dependencies这个变量。
@Retention(RUNTIME)
@Target(TYPE)
@Documented
public @interface Component {
Class<?>[] modules() default {};
Class<?>[] dependencies() default {};
}
用法用法也非常的简单,直接指定需要依赖的Component就行。 我们定义一个UserComponent来依赖原来的AppComponent。
@UserScope
@Component(modules = {UserModule.class},dependencies = AppComponent.class)
public interface UserComponent {
}
这样我们定义的这个UserComponent就拥有了AppComponent的所有功能。和gradle模块直接的依赖是类似的。Dagger规定,不同的Component要有不同的Scope,依赖的时候必须有scope,不然报unscoped的错误,
UserComponent (unscoped) cannot depend on scoped components:
所以我们自定义了一个UserScope专门给UserComponent,UserScope的实现和Singleton一样,只是换了一个名字。
@Scope
@Documented
@Retention(RUNTIME)
public @interface UserScope {}
现在贴出代码来说明一些问题,代码必须要写成下面的样子。后面解释。
@UserScope
@Component(modules = {UserModule.class},dependencies = AppComponent.class)
public interface UserComponent {
void inject(MainActivity mainActivity);
void inject(SecondActivity secondActivity);
}
使用dependencies指定依赖类,定义了两个inject方法,被依赖的Component不需要写这两个方法了,移交给UserComponent。你写了也没事,并不会报错。因为在实际使用中不太可能直接用AppComponent这个组件。既然用不到,写了也没用。非要用就写一个吧。也没事。还有一个需要注意的是@UserScope这个注解,UserComponent使用了这个注解,那么他的modules的方法也要写上这个注解。不然报错。
@Singleton
@Component(modules = {NetModule.class })
public interface AppComponent {
Retrofit retrofit();
RepoService repoService();
OkHttpClient okHttpClient();
}
这里需要注意的是在组件依赖的时候,被依赖的组件的模块的provide人方法全部要在组件里面声明,不然会报错,其实这个设计比较糟糕,完全是不需要的,你Dagger都是可以知道有哪些方法是被@Provider注解的,为什么不帮我们内部实现就好了?
@Module
public class UserModule {
@UserScope
@Provides
public User provideUser(){
return new User();
}
@UserScope
@Provides
public Repo provideRepo(){
return new Repo("xzh");
}
}
UserModule,比较简单,用户这个业务功能可能使用到的对象都在这里提供了。 下面进行最关键的一步——依赖注入。我们分别在MainActivity和SecondActivity里面使用UserComponent实现注入。
public class MainActivity extends AppCompatActivity {
@Inject
User user;
@Inject
User user2;
@Inject
Retrofit retrofit;
@Inject
OkHttpClient client;
@Inject
RepoService repoService;
@Inject
RepoService repoService2;
@Inject
RepoService repoService3;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
DaggerUserComponent.builder()
.appComponent(App.getAppComponent())
.build()
.inject(this);
Log.d("MainActivity", "user:" + user);
Log.d("MainActivity", "user2:" + user2);
Log.d("MainActivity", "retrofit:" + retrofit);
Log.d("MainActivity", "repoService1:" + repoService);
Log.d("MainActivity", "repoService2:" + repoService2);
Log.d("MainActivity", "repoService3:" + repoService3);
startActivity(new Intent( MainActivity.this,SecondActivity.class));
}
代码和前面的一样,只是这次我们使用的是DaggerUserComponent这个实现类,而且我们这次没有使用create方法,而是使用了builder方法,为什么不用create方法了呢?因为在使用组件依赖后,Dagger就不提供create方法了,只提供builder实现。create方法只适合在简单的情况下使用,一组件依赖,就复杂了,需要用户自己build,Dagger只能帮你到这了。最主要原因就是appComponent(App.getAppComponent())这个方法,这是设置需要依赖组件的方法,我们可能依赖多个组件,Dagger不知道我们现在想用哪个,所以不能给我们简单的全部用一个create就搞定了。下面我们看看SecondActivity的代码。
public class SecondActivity extends AppCompatActivity {
@Inject
User user;
@Inject
User user2;
@Inject
RepoService repoService;
@Inject
RepoService repoService2;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_second);
DaggerUserComponent.builder().appComponent(App.getAppComponent()).build().inject(this);
Log.d("SecondActivity", "user:" + user);
Log.d("SecondActivity", "user2:" + user2);
Log.d("SecondActivity", "repoService:" + repoService);
Log.d("SecondActivity", "repoService2:" + repoService2);
}
}
和MainActivity是差不多的,我们注入了几个变量,用于测试。 到目前为止,组件依赖已经全部完成了,我们来运行一下代码。
D/MainActivity: user:com.xzh.androidbase.mvp.model.entry.User@b4dbe39
D/MainActivity: user2:com.xzh.androidbase.mvp.model.entry.User@b4dbe39
D/MainActivity: retrofit:retrofit2.Retrofit@9258c7e
D/MainActivity: repoService1:retrofit2.Retrofit$1@d3f65df
D/MainActivity: repoService2:retrofit2.Retrofit$1@d3f65df
D/MainActivity: repoService3:retrofit2.Retrofit$1@d3f65df
D/SecondActivity: user:com.xzh.androidbase.mvp.model.entry.User@e8b7e6b
D/SecondActivity: user2:com.xzh.androidbase.mvp.model.entry.User@e8b7e6b
D/SecondActivity: repoService:retrofit2.Retrofit$1@d3f65df
D/SecondActivity: repoService2:retrofit2.Retrofit$1@d3f65df
这里我们重点关注repoService和user变量,repoService不管在MainActivity还是在SecondActivity都是单例,而user在MainActivity里面是一个单例,在SecondActivity里面是另一个单例。也就是所谓的局部单例。而我们使用的依赖组件是UserComponent,UserComponent是一个局部单例组件,所以user对象是一个局部单例,但他依赖了AppComponent,而AppComponent是一个全局单例,所以repoService变量是一个全局单例,这样就实现一个组件既有全局单例,又有局部单例的功能。是不是非常的神奇?还是有点难度的,都写几遍就明白了。
如何让UserComponent也变成全局变量呢?非常的简单,和AppComponent一样,把UserComponent写到Application里面就可以了。如下:
public class App extends Application {
static AppComponent appComponent= DaggerAppComponent.create();
static UserComponent userComponent;
public static UserComponent getUserComponent() {
return userComponent;
}
public static AppComponent getAppComponent() {
return appComponent;
}
@Override
public void onCreate() {
super.onCreate();
userComponent = DaggerUserComponent.builder().appComponent(appComponent).build();
}
}
这样就可以实现UserComponent为全局变量。然后在MainActivity和SecondActivity里面都调用 App.getUserComponent().inject(this);这行代码就可以了。 到这里组件依赖这个最难的点就完成了。
|