前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >快刀斩乱码—— Dagger2没有想象的那么难

快刀斩乱码—— Dagger2没有想象的那么难

作者头像
用户1907613
发布2018-07-20 15:27:42
1K0
发布2018-07-20 15:27:42
举报
文章被收录于专栏:Android群英传Android群英传

前言

本篇文章是读者『sososeen09』的投稿,Android群英传刚刚开放投稿,有什么好的建议和意见,还请大家多多和我交流,继续欢迎大家多多投稿~

本篇文章讲解的是Dagger2,相信很多开发者对它都并不陌生,我们公司的内部分享,也有同事介绍过Dagger2。这篇文章并不是讲解Dagger2的基础使用,而是通过讲解它的使用以及套路,帮助大家更好的理解如何高效的使用Dagger2,相信大家看完,一定会对Dagger2的理解更加深刻!

谷歌开发维护的Dagger2出来有很长时间了,目前在很多开源项目上也能看到它的身影。看了一些文章和项目,发现Dagger2的入门虽然有些难,但还是有一些规律可循的。

对于开源的第三方项目,我认为都是有套路可循的,找到这个套路,入门就不会难了,难的是如何更好的在实际开发项目中灵活运用。而灵活运用必然是建立在对这些开源框架深刻理解的基础之上。

关于Dagger2这种依赖注入框架的好处在这只简单的提一下

  • 依赖的注入和配置独立于组件之外
  • 依赖对象是在一个独立、不耦合的地方初始化。当初始化方式改变的时候修改的代码少。
  • 依赖注入使得单元测试更加简单。

那么Dagger2相对于其他的依赖注入框架,有哪些有点和缺点呢?

优点:

  • 编译期生成代码,生成的代码像手写的一样。而且如果有错误会在编译期报出。
  • 错误可追踪
  • 易于调试。

缺点:

  • 缺少灵活性,很多代码要按照既定的规则写
  • 没有动态机制。

下面会展开对Dagger2的介绍,看看Dagger2都有哪些套路。文中的代码都是从自己写的一个Demo中提取,文末会给出项目地址。

1 Dagger2的注解

想要理解Dagger2,首先要理解Dagger2中的注解,至少先了解一下,否则理解Dagger2会有障碍。Dagger2的注解比较多,但主要的会有下面7种。

  • @Inject:@Inject注解有两个作用,1是在需要依赖的类(下面这样的类都会称为目标类)中标记成员变量告诉Dagger这个类型的变量需要一个实例对象。2是标记类中的构造方法,告诉Dagger我可以提供这种类型的依赖实例。
  • @Provide: 对方法进行注解,都是有返回类型的。用来告诉Dagger我们想如何创建并提供该类型的依赖实例(一般会在方法中new出实例)。用@Provide标记的方法,谷歌推荐采用provide为前缀。
  • @Module: @Module这个注解用来标记类(一般类名以Module结尾)。Module主要的作用是用来集中管理@Provide标记的方法。我们定义一个被@Module注解的类,Dagger就会知道在哪里找到依赖来满足创建类的实例。modules的一个重要特征是被设计成区块并可以组合在一起。(例如可以在App中看到多个组合在一起的modules)
  • @Component:Components是组件,也可以称为注入器。是@Inject@Module之间的桥梁,主要职责是把二者组合在一起。@Component注解用来标记接口或者抽象类。所有的components都可以通过它的modules知道它所提供的依赖范围。一个Component可以依赖一个或多个Component,并拿到被依赖Component暴露出来的实例,Component的dependencies属性就是确定依赖关系的实现。
  • @Scope: 作用域。Scopes非常有用,Dagger2通过自定义注解来限定作用域。这是一个非常强大的功能,所有的对象都不再需要知道怎么管理它自己的实例。Dagger2中有一个默认的作用域注解@Singleton,通常在Android中用来标记在App整个生命周期内存活的实例。也可以自定义一个@PerActivity注解,用来表明生命周期与Activity一致。换句话说,我们可以自定义作用域的粒度(比如@PerFragment, @PerUser等等)。
  • @Qualifier: 限定符,也是很有用。当一个类的类型不足以标示一个依赖的时候,我们就可以用这个注解。例如,在Android中,我们需要不同类型的Context,我们可以自定义标识符注解“@ForApplication”“@ForActivity”。这样的话,当注解一个Context的时候,我们可以用这个标识符来告诉Dagger我们想提供哪一种Context。Dagger2里面已经存在一个限定符@Named注解。
  • @SubComponent:如果我们需要父组件全部的提供对象,这时我们可以用包含方式而不是用依赖方式,相比于依赖方式,包含方式不需要父组件显式显露对象,就可以拿到父组件全部对象。且SubComponent只需要在父Component接口中声明就可以了。

2 Dagger2的套路

2.1 最简单的运用

最简单的Dagger2运用只采用两个注解@Inject和@Component即可。因为本身@Inject就自带两个作用。 如一个User类:

代码语言:javascript
复制
public class User {
    public String name;
    //用这个@Inject表示来表示我可以提供User类型的依赖
    @Inject
    public User() {
        name = "sososeen09";
    }
    public String getName() {
        return name;
    }
}

在需要依赖的的目标类中标记成员变量,在这里我们这个目标类是OnlyInjectTestActivity。

代码语言:javascript
复制
@Inject //在目标类中@Inject标记表示我需要这个类型的依赖      
User mUser;

在Component中,Component内有一个方法是inject(OnlyInjectTestActivity onlyInjectTestActivity),参数OnlyInjectTestActivity表示目标类,也就是把依赖实例注入该类中,必须精确,不能用父类代替。查看了一下编译后生成的代码,最后给变量赋值按照“类名.变量”来的。比如我们需要给mUser赋值,那么调用inject方法后,是按照“OnlyInjectTestActivity.mUser=xxx”来的。至于inject这个方法名是可以改的,但是谷歌推荐用inject。

代码就写好了,此时Make Project就会在build文件夹内生成对应的代码。我们的OnlyInjectComponent接口会生成一个以Dagger为前缀的DaggerOnlyInjectComponent类。

采用这个DaggerOnlyInjectComponent就能完成依赖对象的注入。可以在Activity的onCreate方法中调用如下代码,初始化注入。这样的话OnlyInjectTestActivity 中的成员变量mUser就完成了注入过程(也就是变量赋值过程)。

代码语言:javascript
复制
DaggerOnlyInjectComponent.builder().build().inject(this);

整个依赖注入过程就结束了,是不是很简单。 @Inject提供依赖虽然很简单,但是它也有缺陷:

  • 只能标记一个构造方法,如果我们标记两个构造方法,编译的时候就会报错。因为不知道到底要用哪一个构造提供实例。
  • 不能标记其它我们自己不能修改的类,如第三方库,因为我们没办法用@Inject标记它们的构造函数。

举个例子,还是User类,有一个带参的构造方法,

代码语言:javascript
复制
@Inject
public User(String name) {
    this.name = name;
}

如果用@Inject标记带参的构造方法,如String类型。那么这个String类参数也需要依赖,也就是说需要其它地方告诉Dagger可以提供一个String类型的对象。这个时候@Inject就无能为力了,你没办法修改String类给它的构造方法加上@Inject标记啊。所以必须要用我们另一个强大的标记@Module了。

2.2 采用@Module提供依赖

采用@Module标记的类提供依赖是一个常规套路,我们在项目中运用最多的也是这种方式。前面已经提到,@Module标记的类主要起到一个管理作用,真正提供依赖实例靠的是@Provides标记的带返回类型的方法。

这次以一个Person类为例,代码跟User类似,构造方法没有用@Inject标记。目标类中需要给一个Person类型的成员变量mPserson赋值。代码就不贴出来了,具体的可以查看原文链接或项目。

我们用Module提供Person实例,Module代码如下:

代码语言:javascript
复制
@Module
public class DataModule {
    @Provides
    Person providePerson() {
        return new Person();
    }
}

上面的代码也算是一个固定套路了,用@Module标记类,用@Provides标记方法。如果想用Module提供实例,还要有一个Component,如我们下面的PersonComponent 。这个PersonComponent 与纯粹用@Inject方式提供依赖不同,还需要有一个modules属性指向DataModule 。这是告诉Component我们用DataModule 提供你想要的类型的实例。其它的方式相同。

代码语言:javascript
复制
@Component(modules = DataModule.class)
public interface PersonComponent {
    void inject(ModuleTestActivity moduleTestActivity);
}

编译之后,我们就可以在目标类ModuleTestActivity 中进行初始化注入了。

与纯粹用@Inject提供实例不同。新增加了一个dataModule方法,参数是DataModule类型的。因为PersonComponent需要依赖DataModule提供实例,当然也需要一个DataModule对象了。在这里,需要说明一点:如果DataModule只有一个默认的无参构造方法,我们是可以不用调用dataModule方法的,而且此时我们还可以用一个更简单的方式来替代,采用create()方法。之前讲的纯粹用@Inject提供依赖实例的方式也可以这样。

代码语言:javascript
复制
DaggerPersonComponent.create().inject(this);

这样的话,依赖注入过程结束。mPerson已经被赋值。

完成上面两步之后我们会不会有这样的思考:如果同时有@Module和@Inject构造方法来提供同一类型的实例,Dagger会调用哪个呢?这就牵涉到@Module和@Inject的优先级问题了。

2.3 @Module和@Inject的优先级问题

总结一句话就是:在提供依赖对象这一层面上,@Module级别高于@Inject。

虽然写的简单,但也是经过验证的,请大家放心这个结论。

2.4 初始化依赖实例的步骤

讲完了@Mudule和@Inject的优先级问题,我们可以总结一下Dagger是如何查找所需的依赖实例进行注入了。

步骤如下:

  1. 查找Module中是否存在创建该类型的方法(前提是@Conponent标记的接口中包含了@Module标记的Module类,如果没有则直接找@Inject对应的构造方法)
  2. 若存在方法,查看该方法是否有参数
    • 若不存在参数,直接初始化该类的实例,一次依赖注入到此结束。
    • 若存在参数,则从步骤1开始初始化每个参数
  3. 若不存在创建类方法,则查找该类型的类中有@Inject标记的构造方法,查看构造方法中是否有参数
    • 若构造方法中无参数,则直接初始化该类实例,一次依赖注入到此结束。
    • 若构造方法中有参数,从步骤1依次开始初始化每个参数。

如果你要问:我既没有@Module提供的实例,也没有@Inject标记的构造方法会怎样?很简单,编译期就会报错。 Dagger2的报错提醒还是很好的,能帮你快速的查找出问题所在。

2.5 @Qualifier限定符有什么神奇的作用

@Qualifier这个限定符在项目中也会比较有用,比如之前讲的在Android中同样的Context,有ApplicationContext还有Activity的Context,就可以用自定义的“@ForApplication”“@ForActivity”限定符来表示。Dagger2中已经有一个定义好的限定符@Named,长的是这个样子:

代码语言:javascript
复制
@Qualifier
@Documented
@Retention(RUNTIME)
public @interface Named {
    /** The name. */
    String value() default "";
}

下面还是以Person为例,并且我们自定义一个限定符来看看这个东西具体如何使用,Person有两个构造方法。

代码语言:javascript
复制
public Person(String sex) {
    this.sex = sex;
}

public Person() {
    sex = "太监";
}

可以看到,默认的Person对象是一个“太监”,那么我想要一个“妹子”,还想自定义一个,如何区分呢?

我们先仿造@Named自定义一个限定符@PersonQualifier:

代码语言:javascript
复制
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
public @interface PersonQualifier {
}

在DataModule,我们额外提供“妹子”和”qualifier sex”,代码如下:

Component我们就不贴出来了,跟上面一样。在需要依赖的类中:

代码语言:javascript
复制
@Inject
Person mPerson;

@Inject
@Named("female")
Person mPersonFemale;

@Inject
@PersonQualifier
Person mPersonQualifier;

调用DaggerPersonComponent初始化注入之后,我们就可以看到,所有的成员变量都正确赋值了。

2.6 @Scope作用域怎么用

个人觉得,@Scope的作用主要是在组织Component和Module的时候起到一个提醒和管理的作用。 Dagger2中有一个默认的作用域@Singleton,是这么写的:

代码语言:javascript
复制
@Scope
@Documented
@Retention(RUNTIME)
public @interface Singleton {}

乍一看到Singleton,都会觉得Dagger2这么吊,标记一下就能创建单例了?后来研究了一下发现,这个@Singleton并没有创建单例的能力,或者也可以说不是我们常规用的那种单例,直接用AClass.getInstance()就能获取一个AClass的一个全局单例。

@Singleton的使用也很简单,在Module内的provide方法加上这个注解即可,比如我们想提供一个SingletonTestEntity实体对象。在DataModule中,就可以这么写:

代码语言:javascript
复制
@Module
public class DataModule {
    @Provides
    @Singleton
    SingletonTestEntity provideSingletonTestEntity() {
        return new SingletonTestEntity("测试单例");
    }
}

有一个SingletonTestComponent ,我们之前说过@Component可以标注接口,也可以标注抽象类,我们就把这个SingletonTestComponent 改成了抽象类。

需要说明的是:DataModule中的SingletonTestEntity 使用@Singleton标注了,那么对应的Component也必须采用@Singleton标注,表明它们的作用域一致,否则编译的时候会报作用域不同的错误。

我们新建一个SingletonTestActivity,显示mSingletonTestEntity这个对象,有一个Button用于启动一个新的SingletonTestActivity,这样我们就可以看每次这个mSingletonTestEntity是不是同一个,是的话当然就能说明我们创建的这个实体对象是单例了。

我一开始是这么初始化的:

代码语言:javascript
复制
DaggerSingletonTestComponent.builder().build().inject(this);

然后我发现每次启动新的Activity,拿到的SingletonTestEntity不是同一个,让我很困惑,还以为是用的姿势不对。后来研究了一下生成的代码,也查了一些文章,发现真的是我用的姿势不对。初始化依赖注入应该这么写,SingletonTestComponent必须是一个单例的:

代码语言:javascript
复制
SingletonTestComponent.getInstance().inject(this);

这样的话,用这个单例的Component注入器去注入的依赖才能算是单例的。

说到这大家可能也看到了,这怎么能是单例呢?我们常规理解的单例是类在虚拟机中只有一个对象。而我们这个依赖实例其实只是每次都由同一个Component注入器对象提供。如果重新生成一个Component对象,注入的依赖实例就不再是同一个。

我们还可以仿造@Singleton自定义一个作用域,如@PerActivity,用来表示跟Activity的生命周期一致。具体的用法就不再介绍了,跟@Singleton用法一样,项目中可以看。

小结一下:

  • 想要用Component只提供同一个实例对象,就必须保证Component只初始化一次
  • @Singleton并没有创建单例的能力

2.7 重点和难点——组织Component

通过上述的讲解可以发现,Dagger2也没有想象的那么难啊。但是不得不说,Dagger2入门并不难,想要灵活运用就不容易了。主要的原因就是在实际开发中我们要好好的组织Component,那么多页面,那么多类,我们怎么写Component就有学问了。Component有3种组织方式:

  • 依赖方式——一个Component可以依赖一个或多个Component,采用的是@Component的dependencies属性。
  • 包含方式——这里就用到了我们@SubComponent注解,用@SubComponent标记接口或者抽象类,表示它可以被包含。一个Component可以包含一个或多个Component,而且被包含的Component还可以继续包含其他的Component。说起来跟Activity包含Fragment方式很像。
  • 继承方式——用一个Component继承另外一个Component。

下面这张图,是Android-CleanArchitecture项目Component组织方式:

可以看到这么划分的思想是:

  • 我们需要一个ApplicationComponent,管理在App的全局实例,保证在App生命周期内,对象只有一个。例如网络请求的全局HttpClient。
  • ActivityComponent: 负责管理生命周期跟Activity一样的组件。
  • UserComponent: 继承于ActivityComponent的组件,通常会在Activity内部的Fragment中使用。

说到这,我想提一下上面为了演示@Singleton的用法,我们并没有在Application中进行初始化。个人觉得,实际开发中用@Singleton标记来表示在App生命周期内全局的对象,然后用自定义的@PerActivity、@PerFragment等来表示跟Activity、Fragment生命周期一致比较好。

现在我们采用依赖、包含、继承的方式来演示Component的组织方式。就提供一个全局的ApplicationContext好了,只是演示,没必要那么复杂。

我们有个AppModule,用来提供一个App生命周期内全局的Context,AppModule的初始化依赖于Appliation。

代码语言:javascript
复制
//构造依赖Appliation
public AppModule(Application application) {
    this.application = application;
}

@Provides
@Singleton
Context getAppContext() {
    return application;
}

AppComponent是这样的,

代码语言:javascript
复制
@Singleton
@Component(modules = {AppModule.class})
public interface AppComponent {
     //其他的依赖想要用这个Context,必须显式的暴露。
        Context context();
}

想要其它依赖这个AppComponent的Component并使用使用全局的Appliation Context,我们必须显式地暴露出去。

这个AppComponent接口内没有inject方法,因为具体地注入哪个类,是由依赖它的Component决定的。

我们自定义Appliation,在onCreate中初始化一个单例AppComponent,并提供方法返回这个AppComponent单例对象。

再次强调:这个AppConponent只能初始化一次

2.7.1 依赖

现在我们有一个ActivityComponent,需要依赖这个AppComponent ,那么写出来是这个样子:

Component依赖另一个Component,它们的作用域不能相同。所以我们自定义了一个@PerActivity作用域。

我们的这个ActivityComponent本身也可以需要Module提供依赖实例,如ActModule,这个ActModule没有作用域。内部有个@Provides标记的方法用来返回ActEntity类型的对象。至于代码我们就不贴出来了,很简单。

初始化注入的时候我们会发现多了一个appComponent方法用来传入AppComponent类型的参数,这个与传递Module参数的方法形式类似。

2.7.2 包含

上面我们也提到了,包含的方式不需要父Component显示的暴露对象就可以拿到它能提供的所有依赖。在这点上要比依赖方式好一点。在ActivityComponent中我们可以包含其它的Component。只需有一个返回被包含Component类型的方法就好。

代码语言:javascript
复制
 //包含SubComponent
ActSubComponent getActSubComponent();

上面的ActSubComponent 是被包含,它需要有个@Subcomponent注解。如果是包含的方式,作用域可以与包含它的Component一致。这点跟依赖方式不一样。

代码语言:javascript
复制
@Subcomponent
@PerActivity 
public interface ActSubComponent {
    void inject(SubFragment subFragment);
}

一般会在Activity中初始化这个ActivityComponent对象。然后我们在Activity内部的Fragment可以拿到对应的这个SubComponent。

2.7.3 继承

如果AComponent继承了BComponent,那么AComponent就必须提供BComponent中需要的Module。

下面的例子是ExtendTestComponent继承了ActivityComponent,需要提供ActModule和AppModule。 有的人可能会问ActivityComponent并没有AppModule啊,那是因为ActivityComponent依赖了AppComponent,由AppComponent提供了AppModule。

例子中的ExtendTestComponent有@Singleton标记,这是因为AppModule中有@Singleton作用域。如果ActModule中有一个@PerActivity作用域的话,这个Component还必须要再加上@PerActivity。

初始化注入是这个样子,多了两个方法appModule()和actModule()。

哦了,先到这吧。

3 总结

通过上面的内容,至少可以了解Dagger2中常用的一些注解以及组织方式,在这里做一下简单的总结:

  • @Module提供依赖的优先级高于@Inject
  • @Singleton并不是真的能创建单例,但我们依然可以保证在App的生命周期内一个类只存在一个对象。@Singleton更重要的作用是通过标记提醒我们自己来达到更好的管理实例的目的。
  • Component的作用域必须与对应的Module作用域一致,如果@Module没有标记作用域,就不影响。
  • Component和依赖的Component作用域范围不能一样,否则会报错。一般来讲,我们应该对每个Component都定义不同的作用域。
  • 由于@Inject,@Module和@Provides注解是分别验证的,所有绑定关系的有效性是在@Component层级验证。(在这里提一下,本文没有讲这个具体过程)

本文内容都是个人理解与实践,难免有错误和遗漏之处,欢迎指正,共同学习。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2016-11-02,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 群英传 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1 Dagger2的注解
  • 2 Dagger2的套路
    • 2.1 最简单的运用
      • 2.2 采用@Module提供依赖
        • 2.3 @Module和@Inject的优先级问题
          • 2.4 初始化依赖实例的步骤
            • 2.5 @Qualifier限定符有什么神奇的作用
              • 2.6 @Scope作用域怎么用
                • 2.7 重点和难点——组织Component
                  • 2.7.1 依赖
                  • 2.7.2 包含
              • 3 总结
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档