Android中Dagger2的使用

Dagger2是一个在Java和android中使用的完全静态的编译时依赖注入框架。它改编了由Square创建的早期版本,现在由Google维护。
dagger2的优点

    首先说下dagger2的优点,为什么用dagger2(额,也可以说我看着大神用所以我也用吧,哈哈),掘金上的这篇文章是个很好的引入的例子,总结下来大概有三点:

1.增加开发效率、省去重复的简单体力劳动
    首先new一个实例的过程是一个重复的简单体力劳动,dagger2完全可以把new一个实例的工作做了,因此我们把主要精力集中在关键业务上、同时也能增加开发效率上。
省去写单例的方法,并且也不需要担心自己写的单例方法是否线程安全,自己写的单例是懒汉模式还是饿汉模式。因为dagger2都可以把这些工作做了。
2.更好的管理类实例
    每个app中的ApplicationComponent管理整个app的全局类实例,所有的全局类实例都统一交给ApplicationComponent管理,并且它们的生命周期与app的生命周期一样。Module中管理着所对应页面所依赖的所有类实例
3.解耦
假如不用dagger2的话,一个类的new代码是非常可能充斥在app的多个类中的,假如该类的构造函数发生变化,那这些涉及到的类都得进行修改。设计模式中提倡把容易变化的部分封装起来。
我们用了dagger2后,假如是通过用Inject注解标注的构造函数创建类实例,则即使构造函数变的天花乱坠,我们基本上都不需要修改任何代码。
假如是通过工厂模式Module创建类实例,Module其实就是把new类实例的代码封装起来,这样即使类的构造函数发生变化,只需要修改Module即可。

Module的构造函数也会发生变化,发生变化后,相应的new Module的类也发生变化,这就没有达到解耦的效果。首先解耦不是说让类之间或模块之间真的一点关系都没有了,解耦达到的目的是让一个类或一个模块对与自己有关联的类或模块的影响降到最低,不是说这种影响就完全没有了,这是不可能的。

解耦还有个好处,就是方便测试,若需要替换为网络测试类,只需要修改相应的Module即可。

下面说一下Dagger2中的注解
@Inject
作用:用来实例化对象
场景(两种):
class Thermosiphon implements Pump {
  private final Heater heater;

  @Inject
  Thermosiphon(Heater heater) {
    this.heater = heater;
  }
}
class CoffeeMaker {
  @Inject Heater heater;
  @Inject Pump pump;
}
public class MainFragment extends Fragment{
    @Inject
    MainPresenter mainPresenter;
    @Inject
    ToastUtil toastUtil;
    ...
}

就是说在需要依赖的地方使用这个注解(第一种场景:你用它告诉Dagger这个 构造方法,成员变量或者函数方法需要依赖注入。这样,Dagger就会构造一个这个类的实例(第二种场景:如何构造呢,就需要再类的构造方法上声明Inject注解,告诉Dagger,这个用来实例化对象)并满足他们的依赖。

@Module
@ContributesAndroidInjector
@Provides

@Binds

这四个一般情况是一起使用的,举个例子(这是例子链接

@Module
public abstract class AddEditTaskModule {
    @Provides
    @ActivityScoped
    @Nullable
    static String provideTaskId(AddEditTaskActivity activity) {
        return activity.getIntent().getStringExtra(AddEditTaskFragment.ARGUMENT_EDIT_TASK_ID);
    }

    @Provides
    @ActivityScoped
    static boolean provideStatusDataMissing(AddEditTaskActivity activity) {
        return activity.isDataMissing();
    }

    @FragmentScoped
    @ContributesAndroidInjector
    abstract AddEditTaskFragment addEditTaskFragment();

    @ActivityScoped
    @Binds
    abstract AddEditTaskContract.Presenter taskPresenter(AddEditTaskPresenter presenter);
}

Module其实是一个简单工厂模式,Module里面的方法基本都是创建类实例的方法。所以我们定义一个类,用@Module注解,这样Dagger在构造类的实例的时候(如何构造呢?通过@Provides注解标识一个方法,根据方法的返回值来判断是构造哪个类的实例),就知道从哪里去找到需要的依赖。
那为什么有了Inject还需要Module呢?

  • Interfaces can’t be constructed.
  • Third-party classes can’t be annotated.
  • Configurable objects must be configured!
  • 此外,关于module中providers还有好玩的问题,可以看看例子

Binds注解用在这种情况:当参数和返回值类型相同时,将方法写成抽象方法,用Binds注解。

当Binds和Provides两种注解同时出现在同一个Module中时需要做下处理,有两种方案可以选择
1.最简单的方式就是将@Provides标示的方法设置成static的
2.如果不使用static的话,那么就将所有的@Binds修饰的抽象方法已到一个抽象的Module中

ContributesAndroidInjector呢?(这个注解可以先了解,因为它会涉及到后面讲解到的其他的注解)这个注解是2.11版本之后才有的,他的作用是“Generates an AndroidInjector for the return type of this method”,就是说ContributesAndroidInjector这个注解帮助我们生成了一个注射器,什么注射器呢,方法的返回值类型(例子中的AddEditTaskFragment)的注射器。也就是说通过将方法addEditTaskFragment标识@ContributesAndroidInjector注解会生成如下代码:

@Module(subcomponents = AddEditTaskModule_AddEditTaskFragment.AddEditTaskFragmentSubcomponent.class)
public abstract class AddEditTaskModule_AddEditTaskFragment {
  private AddEditTaskModule_AddEditTaskFragment() {}

  @Binds
  @IntoMap
  @FragmentKey(AddEditTaskFragment.class)
  abstract AndroidInjector.Factory<? extends Fragment> bindAndroidInjectorFactory(
      AddEditTaskFragmentSubcomponent.Builder builder);

  @Subcomponent
  @FragmentScoped
  public interface AddEditTaskFragmentSubcomponent extends AndroidInjector<AddEditTaskFragment> {
    @Subcomponent.Builder
    abstract class Builder extends AndroidInjector.Builder<AddEditTaskFragment> {}
  }
}

可以看到ContributesAndroidInjector就是用来简化Subcomponent的书写,在2.11之前,每一个Fragment或者Activity都需要有自己的Component然后在Component中声明inject方法,最后在Fragment或者Activity中进行注入,而现在ContributesAndroidInjector一个注解完成了这些步骤,大大简化了dagger模板代码的书写。

不过需要注意的是ContributesAndroidInjector注解标识的方法必须是抽象方法并且返回值是具体的android framework的类型(比如Activity或者Fragment),此外,这个方法不能有参数。

额,如果你注意到了上面的代码中还有"ActivityScope"和"FragmentScope"这两个注解,别急,这属于Scope注解,后面会有讲解的。

@Component @Component.Builder @SubComponent @SubComponent.Builder
@Singleton
@Component(modules = {TasksRepositoryModule.class,
        ApplicationModule.class,
        ActivityBindingModule.class,
        AndroidSupportInjectionModule.class})
public interface AppComponent extends AndroidInjector<ToDoApplication> {

    @Component.Builder
    interface Builder {

        @BindsInstance
        AppComponent.Builder application(Application application);

        AppComponent build();
    }
}

Component也是一个注解类,一个类要想是Component,必须用Component注解来标注该类,并且该类是接口或抽象类。它在编译时会产生相应的类的实例来作为提供依赖方和需要依赖方之间的桥梁。
那我们看看这桥梁是怎么工作的:

Component需要引用到目标类的实例,Component会查找目标类中用Inject注解标注的属性,查找到相应的属性后会接着查找该属性对应的用Inject标注的构造函数(这时候就发生联系了),剩下的工作就是初始化该属性的实例并把实例进行赋值。因此我们也可以给Component叫另外一个名字注入器(Injector,事实上Component必须继承的父类就叫做AndroidInjector

*以下知识点可以忽略,因为在2.11版本之后,ContributesAndroidInjector基本上代替了SubComponent的作用。

Component有两种组织方式(关于两种方式的对比可以看这儿和这儿,一种是SubComponent包含方式;另一种是dependencies依赖方式(这种方式只能获取到依赖的 Component 所暴露出来的对象

Component.Builder注解只用在Component标识的接口或抽象类中,固定写法,用来提供Component实例。

SubComponent.Builder同理,只不过是用在SubComponent标准的类或者接口中。

这儿一般情况下还会用到一个注解BindInstance,这个注解用在Component.Builder或者Subcomponent.Builder标示的方法上,表示允许方法的参数可以绑定实例对象。

@Scope
Scope的真正用处就在于Component的组织。
更好的管理Component之间的组织方式,不管是依赖方式还是包含方式,都有必要用自定义的Scope注解标注这些Component,这些注解最好不要一样,不一样是为了能更好的体现出Component之间的组织方式。

  • 正常情况下没有任何Scope标示的Component,注入器每次根据依赖都会新的实例,而如果有Scope标示,那么注入器创建实例后会进行保存,以便在稍后的注射中可能重用。
  • 没有使用Scope标示的Component不能依赖一个使用Scope标示的Component,而且依赖Component的SubComponent必须使用和父Component不同的Scope标示
  • 更好的管理Component与Module之间的匹配关系,编译器会检查 Component管理的Modules,若发现标注Component的自定义Scope注解与Modules中的标注创建类实例方法的注解不一样,就会报错。
  • 可读性提高,如用Singleton标注全局类,这样让程序猿立马就能明白这类是全局单例类。

例如你可以这样定义一个Scope

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

而@SingleTon这是个特殊的Scope,它的注释是这样的

Identifies a type that the injector only instantiates once. Not inherited

意思就是说标识注入器只会初始化一次这种类型。但是他是怎么做到的呢?可以参考这篇文章
@Qualifier

创建类实例有2个维度可以创建:

  • 通过用Inject注解标注的构造函数来创建(以下简称Inject维度)
  • 通过Module中的Providez注解或者Binds等注解来创建(以下简称Module维度)

这2个维度是有优先级之分的,Component会首先从Module维度中查找类实例,若找到就用Module维度创建类实例,并停止查找Inject维度。否则才是从Inject维度查找类实例。所以创建类实例级别Module维度要高于Inject维度。
现在有个问题,基于同一个维度条件下,若一个类的实例有多种方法可以创建出来,那注入器(Component)应该选择哪种方法来创建该类的实例呢?Qualifier注解用来区分同一纬度下两种不同的创建实例的方法

有三个步骤:

1.定义两个不同的Qualifier,标识

@Qualifier
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface Local {

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

}
2.在同一个Module中定义两个返回值相同的方法
    @Singleton
    @Binds
    @Local
    abstract TasksDataSource provideTasksLocalDataSource(TasksLocalDataSource dataSource);

    @Singleton
    @Binds
    @Remote
    abstract TasksDataSource provideTasksRemoteDataSource(FakeTasksRemoteDataSource dataSource);

3.具体使用,用@Remote标识的对象会通过provideTasksRemoteDataSource方法来实例化,用@Local标识的对象会通过provideTasksLocalDataSource方法来实例化

@Inject
    TasksRepository(@Remote TasksDataSource tasksRemoteDataSource,
            @Local TasksDataSource tasksLocalDataSource) {
        mTasksRemoteDataSource = tasksRemoteDataSource;
        mTasksLocalDataSource = tasksLocalDataSource;
    }

当然,在javax.inject包中,定义好了一个可以直接使用的Qualifier,“Named”

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

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

你可以不用自定义Qualifier而是直接使用Named注解他的使用方法和自定义的类似,举个例子:

@Singleton
@Binds
@Named("Local")
abstract TasksDataSource provideTasksLocalDataSource(TasksLocalDataSource dataSource);

@Singleton
@Binds
@Named("Remote")
abstract TasksDataSource provideTasksRemoteDataSource(FakeTasksRemoteDataSource dataSource);
当理解了上面的注解之后,那么我们来创建一个Android App。

额,首先创建例子中的di文件夹以及里面的5个类,这个一般情况下是固定的,然后在Application中继承HasActivityInjector和HasSupportFragmentInjector并进行方法实现(具体看例子就好了),最后再BaseActivity和BaseFragment中做处理。BaseActivity需要继承HasSupportFragmentInjector接口然后 实现方法(和Application类似,因为一般情况下Activity会包含Fragment),然后加上注入代码

 AndroidSupportInjection.inject(this);

注意:BaseActivity的注入代码是加在onCreate()方法中,而Fragment的注入代码是加在onAttach方法中,参考自这儿

例子地址在:https://github.com/fanturbo/TodoApp

参考:
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值