Android-Dagger2andnroidAndroid开发经验谈

都是套路——Dagger2没有想象的那么难

2016-10-30  本文已影响6406人  sososeen09

本文的分析基于dagger2的2.7版本

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

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

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

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

优点:

缺点:

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

1 Dagger2的注解

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

2 Dagger2的套路

2.1 最简单的运用

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

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

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

@Inject //在目标类中@Inject标记表示我需要这个类型的依赖      
User mUser;

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

/**
 * 没有modules和dependencies的情况下,纯粹用@Inject来提供依赖
 */
@Component()
public interface OnlyInjectComponent {
    /**
     * 必须有个目标让Component知道需要往哪个类中注入
     * 这个方法名可以是其它的,但是推荐用inject
     * 目标类OnlyInjectTestActivity必须精确,不能用它的父类
     * 这是Dagger2的机制决定的
     */
    void inject(OnlyInjectTestActivity onlyInjectTestActivity);
}

代码就写好了,此时Make Project就会在build文件夹内生成对应的代码。我们的OnlyInjectComponent接口会生成一个以Dagger为前缀的DaggerOnlyInjectComponent类。
采用这个DaggerOnlyInjectComponent就能完成依赖对象的注入。可以在Activity的onCreate方法中调用如下代码,初始化注入。这样的话OnlyInjectTestActivity 中的成员变量mUser就完成了注入过程(也就是变量赋值过程)。

DaggerOnlyInjectComponent.builder().build().inject(this);

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

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

public class User {
    public String name;
    /**
     * 用@Inject标记的构造函数如果有参数,那么这个参数也需要其它地方提供依赖。
     * 但是@Inject有一个缺陷,就是对于第三方的类无能为力。因为我们不能修改第三方的构造函数,
     * 所以对于String还有其他的一些我们不能修改的类,只能用@Module中的@Provides来提供实例了
     */
    @Inject
    public User(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

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

2.2 采用@Module提供依赖

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

这次以一个Person类为例,Person类如下,构造方法没有用@Inject标记:

public class Person {
    private String sex;

    public Person(String sex) {
        this.sex = sex;
    }

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

    public String getSex() {
        return sex;
    }
}

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

@Module
public class DataModule {

    @Provides
    Person providePerson() {
        return new Person();
    }

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

@Component(modules = DataModule.class)
public interface PersonComponent {
    void inject(ModuleTestActivity moduleTestActivity);
}

ModuleTestActivity 中需要一个Person类型的依赖:

@Inject
Person mPerson;

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

DaggerPersonComponent.builder().dataModule(new DataModule()).build().inject(this);

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

//如果DataModule有一个无参构造方法
DaggerPersonComponent.create().inject(this);

这样的话,依赖注入过程结束。mPerson已经被赋值。
完成上面两步之后我们会不会有这样的思考:如果同时有@Module和@Inject构造方法来提供同一类型的实例,Dagger会调用哪个呢?这就牵涉到@Module和@Inject的优先级问题了。

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

虽然优先级的问题,我们可以直接说出来,但还是亲手做一个实验好了,这样印象必定会更深刻。而且当你告诉别人这个结论的时候,你就可以挺直腰板的说就是这样,不然心里总虚啊。
新建一个实体类PriorityTestEntity,用@Inject标记构造方法,代码如下:

public class PriorityTestEntity {

    private String name;

    @Inject
    public PriorityTestEntity() {
        name = "我是@Inject注解提供的对象";
    }

    public PriorityTestEntity(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

在DataModule中有一个方法,返回值类型是PriorityTestEntity:


@Module
public class DataModule {
    @Provides
    PriorityTestEntity providePriorityTestEntity() {
        return new PriorityTestEntity("我是module提供的对象");
    }

新建Component,PriorityTestComponent,有一个inject()方法,注入目标类
PriorityTestActivity。

@Component(modules = {DataModule.class})
public interface PriorityTestComponent {
    //注入目标类PriorityTestActivity 
    void inject(PriorityTestActivity priorityTestActivity);
}

剩下的就是在PriorityTestActivity 中进行初始化注入了,步骤都是跟之前讲的一样,真真的是套路啊,没什么难度。

在PriorityTestActivity 中
//@Inject标记成员变量
@Inject
PriorityTestEntity mPriorityTestEntity;

//初始化注入
DaggerPriorityTestComponent.create().inject(this);

//调用代码验证
mTvShowUser.setText(mPriorityTestEntity.getName());

//最后TextView上会显示"我是module提供的对象"

总结一句话就是:在提供依赖对象这一层面上,@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,长的是这个样子:

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

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

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

public class Person {
    private String sex;

    public Person(String sex) {
        this.sex = sex;
    }

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

    public String getSex() {
        return sex;
    }
}

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

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

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
public @interface PersonQualifier {
}

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

@Module
public class DataModule {

//一个默认的
    @Provides
    Person providePerson() {
        return new Person();
    }

//采用@Qualifier注解,表示我可以提供这种标识符的Person
    @Provides
    @Named("male")
    Person providePersonMale() {
        return new Person("汉子");
    }

    @Provides
    @Named("female")
    Person providePersonFemale() {
        return new Person("妹子");
    }

    @Provides
    @PersonQualifier
    Person providePersonByQualifier() {
        return new Person("qualifier sex");
    }
}

Component长这个样子:

@Component(modules = DataModule.class)
public interface PersonComponent {
    void inject(ModuleTestActivity moduleTestActivity);
}

在需要依赖的类中

//在ModuleTestActivity中成员变量这样标记。
    @Inject
    Person mPerson;

    //这么多对象,如果需要特定的对象,用@Qualifier标识符注解,@Named是自定义的一个标识符注解
    @Inject
    @Named("male")
    Person mPersonMale;

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

    @Inject
    @PersonQualifier
    Person mPersonQualifier;

然后注入,

 DaggerPersonComponent.builder().dataModule(new DataModule()).build().inject(this);

查看Person对象的性别:

...
mTvShowUser.setText(mPerson.getSex());
...
mTvShowUser.setText(mPersonMale.getSex());
...
mTvShowUser.setText(mPersonFemale.getSex());
...
mTvShowUser.setText(mPersonQualifier.getSex());
...

就可以看到,我们拿到了我们想要的对象。

2.6 @Scope作用域怎么用

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

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

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

下面我们看看,这个@Singleton怎么用,又是如何获取单例的。
我们有一个实体SingletonTestEntity,

public class SingletonTestEntity {
    private String desc;

    @Inject
    public SingletonTestEntity(String desc) {
        this.desc = desc;
    }

    public String getDesc() {
        return desc;
    }
}

在DataModule中,

@Module
public class DataModule {
    @Provides
    @Singleton
    SingletonTestEntity provideSingletonTestEntity() {
        return new SingletonTestEntity("测试单例");
    }
}

有一个SingletonTestComponent ,我们之前说过@Component可以标注接口,也可以标注抽象类,我们就把这个SingletonTestComponent 改成了抽象类。
需要说明的是:DataModule中的SingletonTestEntity 使用@Singleton标注了,那么对应的Component也必须采用@Singleton标注,表明它们的作用域一致,否则编译的时候会报作用域不同的错误。

@Component(modules = {DataModule.class})
@Singleton//这个Component的@Scope要和对应的Module的@Scope一致
public abstract class SingletonTestComponent {
    /**
     * /@Component不仅可以注解接口也可以注解抽象类,为了方便测试单例,把Component改为抽象类,
     * 实际开发中可以在Application中创建单例。
     */
    public abstract void inject(SingletonTestActivity singletonTestActivity);


    /**
     * SingletonTestComponent必须是单例的,
     * 否则怎么能保证不同的Component对象提供同一个依赖实例呢?
     */
    private static SingletonTestComponent sComponent;

    public static SingletonTestComponent getInstance() {
        if (sComponent == null) {
            sComponent = DaggerSingletonTestComponent.builder().build();
        }
        return sComponent;
    }

}

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

//成员变量
@Inject
SingletonTestEntity mSingletonTestEntity;

//展示mSingletonTestEntity这个对象
mTvShowUser.setText(mSingletonTestEntity.getDesc() + ": " + mSingletonTestEntity);

上面少了一步,就是初始化注入,我一开始是这么初始化的:

DaggerSingletonTestComponent.builder().build().inject(this);

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

SingletonTestComponent.getInstance().inject(this);

这样的话,我们的这个注入器SingletonTestComponent就首先实实在在地变成一个单例了,用这个Component去注入的依赖才是单例的。

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

我们还可以仿造@Singleton自定义一个作用域,如@PerActivity,用来表示跟Activity的生命周期一致:

@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface PerActivity {
}

具体的用法就不再介绍了,跟@Singleton用法一样,项目中可以看。

总结一下:想要用Component只提供同一个实例对象,就必须保证Component只初始化一次。

2.7 重点和难点——组织Component

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

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

组织方式.png

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

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

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

Module类是这样的,

@Module
public class AppModule {

    private final Application application;

    public AppModule(Application application) {
        this.application = application;
    }

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

AppComponent是这样的,

@Singleton
@Component(modules = {AppModule.class})
public interface AppComponent {
    /**
     * Exposed to sub-graphs.
     * 其他的依赖想要用这个Context,必须显式的暴露。
     * 因为,其它依赖这个的Component需要Context,然后这个Context会去AppModule中找对应的Context
     * 与方法名无关,只与返回类型有关
     * 举个例子:小弟B依赖大哥A,A有一把杀猪刀。哪天小弟碰上事了,找大哥借一把刀,
     * 如果大哥把刀藏起来不给小弟用,小弟会因为找不到刀用很崩溃的。(程序编译报错),
     * 所以必须是大哥把刀拿出来给小弟用,小弟才能拿出去用啊。(代码正常)
     *
     */
    Context context();
}

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

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

我们自定义Appliation ,

public class App extends Application {
    private static AppComponent sAppComponent = null;

    @Override
    public void onCreate() {
        super.onCreate();
        if (sAppComponent == null) {
            sAppComponent = DaggerAppComponent.builder()
                                              .appModule(new AppModule(this))
                                              .build();
        }
    }

    public AppComponent getAppComponent() {
        //向外界的依赖提供这个AppComponent
        return sAppComponent;
    }
}

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

2.7.1 依赖

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

@PerActivity
//@Singleton //不能与依赖的AppComponent的作用域相同,否则会报错
@Component(dependencies = AppComponent.class, modules = ActModule.class)
public interface ActivityComponent {

    void inject(DependenceTestActivity DependenceTestActivity);

    void inject(SubComponentTestActivity subComponentTestActivity);

    //包含SubComponent,这样的话该SubComponent也可以拿到ActivityComponent中能提供的依赖。
    ActSubComponent getActSubComponent();
}

Component依赖另一个Component,它们的作用域不能相同。所以我们自定义了一个@PerActivity作用域。
我们的这个ActivityComponent本身也可以需要Module提供依赖实例,如ActModule,这个ActModule没有作用域。至于ActEntity的代码,我们就不贴出来了。

@Module
public class ActModule {
    @Provides
    ActEntity getActEntity() {
        return new ActEntity("我是ActEntity");
    }
}

初始化注入是这个样子:

DaggerActivityComponent.builder()
                       .appComponent(((App) getApplication()).getAppComponent())
                       .build()
                       .inject(this);

2.7.2 包含

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

@Subcomponent
@PerActivity //如果是包含的方式,作用域与上一层的Component相同也没关系。采用依赖的方式就不行。
public interface ActSubComponent {
    void inject(SubFragment subFragment);
}

初始化注入是这个样子:
在Activity中

mActivityComponent = DaggerActivityComponent
                    .builder()
                    .appComponent(((App) getApplication()).getAppComponent())
                    .actModule(new ActModule())
                    .build();

然后在Fragment中拿到这个mActivityComponent :

((SubComponentTestActivity) getActivity()).getActivityComponent()
                                          .getActSubComponent()
                                          .inject(this);

2.7.3 继承

我们的ExtendTestComponent继承了ActivityComponent,那么ActivityComponent中需要的Module我们就必须提供。有的人可能会问ActivityComponent并没有AppModule啊,那是因为ActivityComponent依赖了AppComponent,由AppComponent提供了AppModule。

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

/**
 * ExtendTestComponent继承了ActivityComponent,
 * 如果ActivityComponent中的modules定义了创建实例的方法,
 * ExtendTestComponent中也必须提供相应的modules。
 */
@Singleton
@Component(modules = {ActModule.class, AppModule.class})
public interface ExtendTestComponent extends ActivityComponent {
    void inject(ExtendTestActivity extendTestActivity);
}

初始化注入是这个样子:

DaggerExtendTestComponent.builder()
                         .appModule(new AppModule(getApplication()))
                         .actModule(new ActModule())
                         .build()
                         .inject(this);

哦了,先到这吧。

3 总结

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

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

参考文章:

上一篇下一篇

猜你喜欢

热点阅读