【Dagger2】一文让你从Dagger2入门到熟练

1 Dagger2概述

1.1 Dagger2简述

Dagger 是一个可以用于Java及Android平台的依赖注入框架,其注入过程完全是静态的在编译时期完成的(通过编译时产生代码的方式,区别于Spring等框架的依赖反射的方式,反射是基于运行时的)1

build.gradle(app)导入依赖

	implementation 'com.google.dagger:dagger:2.27'
    implementation 'com.google.dagger:dagger-android:2.27'
    implementation 'com.google.dagger:dagger-android-support:2.27'
    annotationProcessor 'com.google.dagger:dagger-android-processor:2.27'
    annotationProcessor 'com.google.dagger:dagger-compiler:2.27'

1.2 依赖注入

依赖注入是一种给一个对象提供其依赖的对象的技术。例如A对象需要依赖B对象,那么此处关键点就是不能在A里面去new B对象,必须通过某种方式将B对象提供给(注入)A中,例如最简单的方式是通过一个 set(B b)方法。

例如:下面这种方式中A对B的依赖就不是以依赖注入的方式获得而是A主动构建的B。

public class A {
    private B b=new B();    
}

而下面这种方式就是将依赖注入到了A类中。

public class A {
    private B b;
	//方法一
	public A(B b){
		this.b = b;
	}
	//方法二
    public void setB(B b) {
        this.b = b;
    }
}

依赖注入最主要的目的就是为了解耦,随即而来的就是可测试性,可维护,可扩展性就大大提高了。以上面情况为例:

第一种方式中A和B紧紧耦合在一起。假设由于业务需要,B的构造函数中多了一个参数,即B的构造方式发生了改变,那么我们就必须到A中去修改相关代码。如果只有A一个类使用了B还好,要是有很多个类依赖了B,那就需要改很多地方。第二种方式就好了很多,不需要去修改A类,只需要修改外部生成B对象的地方就行。

1.3 Dagger2的作用

第一:dagger 是一个依赖注入框架,首要任务当然是解决依赖注入的问题。
第二:dagger主要想通过编译时产生代码的方式来解决那些基于反射的依赖注入框架所存在的缺点,例如性能问题,开发过程中存在的问题。

2 Dagger2中的注解

2.1 @Inject注解

  • 注解在属性中表示该属性需要依赖注入,注解修饰的属性不能使用private修饰,只能用默认、protected或public2
@Inject
Student mStudent;
  • 注解在方法中表示该方法需要依赖注入,注解修饰的方法不能是抽象方法,不能是private修饰的
//@Inject
Student mStudent;
@Inject
public void injectStudent(Student student) {
    mStudent= student;
}
  • 注解在构造方法中表示此类能为Dagger2提供依赖关系,如果有多个构造函数,只能注解一个,否则会报错,如果有多个构造函数,可用其它方法办到,后续再讲。

2.2 @Module注解

该注解与@Provides结合为Dagger2提供依赖关系,用于不能用@Inject提供依赖的地方,如第三方库提供的类,基本数据类型等不能修改源码的情况。

2.3 @Component注解

一般用来注解接口,被注解的接口在编译时会生成相应的实例,实例名称一般以Dagger为前缀,作为所需注入依赖和提供依赖之间的桥梁,把提供的依赖注入到所需注入的依赖中。

两个@Inject注解形成了依赖关系,@Component作为连接这个关系的桥梁存在,寻找到依赖并且注入,并且注入与被注入之间互不干涉,经过编译@Component生成Dagger为前缀的实例,调用实例的方法注入。

2.4 @Provides注解

@Provides仅能注解方法,且方法所在类要有@Module注解。注解后的方法表示Dagger2能用该方法实例对象提供依赖。按照惯例,@Provides方法的命名以provide为前缀,方便阅读管理。

2.5 @Qualifier注解

@Qualifier是一个限定标识符,通常用来解决依赖冲突。Dagger2中,如果Module中有@Provides标注的多个方法同时提供同一类型的依赖,IDE编译时在会报错,通俗的讲就是,Dagger2不知道该用哪个方法提供的依赖,如:

@Module
public class TestModule {
    private String name;

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

    @Provides
    public Student provideStu() {
        return new Student();
    }

    @Provides
    public String provideName() {
        return name;
    }
    
    @Provides
    public Student provideStudent(String name) {
        return new Student(name);
    }
}

    @Inject
    Student mStudent;

如上述事例,在有多个Student构造方法情况下,如果如果没有限定符,则系统不知道使用provideStu还是provideStudent来初始化Student,会报以下错误:

错误: [Dagger/DuplicateBindings] com.crystallake.cod.test.Student is bound multiple times:
@Provides com.crystallake.cod.test.Student com.crystallake.cod.module.TestModule.provideStu()
@Provides com.crystallake.cod.test.Student com.crystallake.cod.module.TestModule.provideStudent(String)
com.crystallake.cod.test.Student is injected at
com.crystallake.cod.test.TestActivity.mStudent
com.crystallake.cod.test.TestActivity is injected at
com.crystallake.cod.component.TestComponent.inject(com.crystallake.cod.test.TestActivity)

只要加入限定符就可以了

@Qualifier
public @interface ChoiceInstance {
    int ZERO_PARAMS = 0;
    int ONE_PARAMS = 1;
    int TWO_PARAMS = 2;
    int THREE_PARAMS = 3;
    int FOUR_PARAMS = 4;

    int choiceType() default ZERO_PARAMS;
}

@Module
public class TestModule {
    private String name;

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

    @ChoiceInstance
    @Provides
    public Student provideStu() {
        return new Student();
    }

    @Provides
    public String provideName() {
        return name;
    }

    @ChoiceInstance(choiceType = ChoiceInstance.ONE_PARAMS)
    @Provides
    public Student provideStudent(String name) {
        return new Student(name);
    }
}

    @Inject
    Student mStudent;

2.6 @Scope注解

@Scope只能用于注解上,我们需要用@Scope自定义作用域注解,控制所提供依赖的生命周期,使提供的依赖可以做到与视图的生命周期相同、局部单例或者是全局单例等。

如下所示:

public class Teacher {
    private String name;
    @Inject
    public Teacher(){
        this.name = "I am a teacher";
    }
    public String getName(){
        return name;
    }
}

@Component
public interface TeacherComponent {
    void inject(TestActivity activity);
}

public class TestActivity extends Activity {
    @BindView(R.id.btn_test)
    public Button btn;
    @Inject
    Teacher mTeacher;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
        ButterKnife.bind(TestActivity.this);

        DaggerTeacherComponent.create().inject(this);
        System.out.println(mTeacher.toString());
        DaggerTeacherComponent.create().inject(this);
        System.out.println(mTeacher.toString());
    }
}

结果:
在这里插入图片描述
从结果看,当没使用@Scope时,每次依赖注入都会新建实例 ,如果我们想每次注入返回的实例都是一样呢?其方法就是通过@Scope注解

  • 先自定义一个@Scope注解
@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface SignLocal {
}
  • 在TeacherComponent上添加@SignLocal
@SignLocal
@Component(modules = TeacherModule.class)
public interface TeacherComponent {
    void inject(TestActivity activity);
}
  • 在TeacherModule的@Provides上添加@SignLocal
@Module
public class TeacherModule {
    @SignLocal
    @Provides
    public Teacher provideTeacher(){
        return new Teacher();
    }
}
  • Teacher
public class Teacher {
    private String name;

    @Inject
    public Teacher(){
        this.name = "I am a teacher";
    }

    public String getName(){
        return name;
    }
}
  • 调用
public class TestActivity extends Activity {
    @BindView(R.id.btn_test)
    public Button btn;
    @Inject
    Teacher mTeacher;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
        ButterKnife.bind(TestActivity.this);

        TeacherComponent component = DaggerTeacherComponent.create();
        component.inject(this);
        System.out.println(mTeacher.toString());
        component.inject(this);
        System.out.println(mTeacher.toString());
    }
}

结果:
在这里插入图片描述
经过其它多种组合测试,在本文的Dagger2版本情况下,必须要在Component上和Provides上加@SignLocal才能在TeacherComponent 同个容器下生成的对象是一样的。

2.7 @Singleton

在Dagger2中,@Singleton注解可以保证被注解的对象全局都是单例。为什么这个注解会有这种效果呢?我们用一个例子来说明。

首先@Singleton注解需要在两处添加,第一处是在创建实例的地方添加

@Module
public class MainModule {
    @Provides
    @Singleton
    static Student provideStudent(){
        return new Student();
    }
    @Provides
    static Teacher provideTeacher(){
        return new Teacher();
    }
}

之所以写了一个provideTeacher是为了待会做对比。第二处需要在Component添加:

@Singleton
@Component(modules = MainModule.class)
public interface MainComponent {
    void inject(MainActivity activity);
}

添加完之后编译下,我们来看看生成的类文件有什么变化。仔细对比会发现,主要的变化在DaggerMainComponent类里面:

 private MainActivity injectMainActivity(MainActivity instance) {
    MainActivity_MembersInjector.injectStudent(instance, provideStudentProvider.get());
    MainActivity_MembersInjector.injectStudent1(instance, provideStudentProvider.get());
    MainActivity_MembersInjector.injectTeacher(
        instance, MainModule_ProvideTeacherFactory.proxyProvideTeacher());
    MainActivity_MembersInjector.injectTeacher1(
        instance, MainModule_ProvideTeacherFactory.proxyProvideTeacher());
    return instance;
  }

Student类添加了@Singleton注解,而Teacher类没有。不同之处在于,Student实例对象是通过provideStudentProvider.get()获得的。而Teacher实例直接通过编译出来的工厂类创建。从这里其实可以分析下,这个provideStudentProvider一定是对新生成的对象做了单例处理。果不其然:

@SuppressWarnings("unchecked")
  private void initialize(final Builder builder) {
    this.provideStudentProvider = DoubleCheck.provider(MainModule_ProvideStudentFactory.create());
  }

这个provideStudentProvider ,是通过DoubleCheck对Student的工厂类进行了包装之后得到的,那么它调用的get方法其实是DoubleCheck里面的get方法,继续点击去看一下:

//这里对返回的对象做了单例的处理
  @Override
  public T get() {
    Object result = instance;
    if (result == UNINITIALIZED) {
      synchronized (this) {
        result = instance;
        if (result == UNINITIALIZED) {
          result = provider.get();
          instance = reentrantCheck(instance, result);
          /* Null out the reference to the provider. We are never going to need it again, so we
           * can make it eligible for GC. */
          provider = null;
        }
      }
    }
    return (T) result;
  }

由此可以得出结论,添加了@Singleton注解之后,会有DoubleCheck这个类去做处理。这样就保证了每次获取的对象的唯一性。

3 Dagger2实例

3.1 @Inject+@Component使用

  • Teacher.java
package com.crystallake.cod.test;

import javax.inject.Inject;

/**
 * Created by yds
 * on 2020/4/26.
 */
public class Teacher {
    private String name;

    @Inject
    public Teacher(){
        this.name = "I am a teacher";
    }

    public String getName(){
        return name;
    }
}

  • TeacherComponent.java
package com.crystallake.cod.component;

import com.crystallake.cod.test.TestActivity;

import dagger.Component;

/**
 * Created by yds
 * on 2020/4/26.
 */
@Component
public interface TeacherComponent {
    void inject(TestActivity activity);
}

  • 调用
package com.crystallake.cod.test;

import android.app.Activity;
import android.os.Bundle;
import android.widget.Button;
import android.widget.Toast;
import com.crystallake.cod.R;
import com.crystallake.cod.component.DaggerTeacherComponent;
import javax.inject.Inject;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;

/**
 * Created by yds
 * on 2020/4/23.
 */
public class TestActivity extends Activity {
    @BindView(R.id.btn_test)
    public Button btn;

    @Inject
    Teacher mTeacher;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
        ButterKnife.bind(TestActivity.this);

        DaggerTeacherComponent.create().inject(this);

    }

    @OnClick(R.id.btn_test)
    public void clickTest() {
        Toast.makeText(TestActivity.this, mTeacher.getName(), Toast.LENGTH_SHORT).show();
    }
}

如上面代码所示,在需要注入依赖的地方(TestActivity中的Teacher上)加上@Inject,然后在提供依赖注入的地方(Teacher的构造函数上)加上@Inject,并声明一个Component接口,就可以实现简单的依赖注入了。Component是需要依赖注入和提供依赖注入的桥梁。需要依赖注入的就好比男单身狗,提供依赖注入的就好比女单身狗,Component就好比是媒婆,她将两个单身狗撮合在了一起。

3.2 @Inject+@Component+@Module挖墙脚

需要依赖注入的地方标记了 @Inject,但提供依赖注入的地方并没有被@Inject标记,@Inject标记了的地方,就好比告诉别人,我是单身的,我需要依赖注入或者可以提供依赖注入,而没有被@Inject标记的地方,就好比是告诉别人,我是有夫之妇,不能被依赖或者提供依赖,那确实想要挖墙脚,撩拨有夫之妇怎么办?那就使用@Module,这就好比锄头,专门用来挖墙脚的。该如何做呢?看下面代码:

  • 第三方代码,不能被修改,也就不能在第三方代码上添加@Inject
public class ThirdClass {
    private String message;

    public ThirdClass() {
        this.message = "This is a third class ,can not change";
    }

    public String getMessage(){
        return message;
    }
}
  • 就算是有锄头,媒婆还是要的,想想潘金莲和西门庆
@Component(modules = ThirdClassModule.class)
public interface ThirdClassComponent {
    void inject(TestActivity activity);
}
  • 媒婆将女方约出来后,可以使用锄头了
@Module
public class ThirdClassModule {
    public ThirdClassModule(){

    }

    @Provides
    public ThirdClass provideThirdClass(){
        return new ThirdClass();
    }
}
  • 锄头挥得好,没有墙脚挖不倒(举个栗子,不要当真)
public class TestActivity extends Activity {
    @BindView(R.id.btn_test)
    public Button btn;

    @Inject
    ThirdClass mThirdClass;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
        ButterKnife.bind(TestActivity.this);

        DaggerThirdClassComponent.create().inject(this);

    }

    @OnClick(R.id.btn_test)
    public void clickTest() {
        Toast.makeText(TestActivity.this, mThirdClass.getMessage(), Toast.LENGTH_SHORT).show();
    }

3.3 @Inject+@Component+@Module+@Qualifier结婚证

@Qualifier就好比结婚证,指定谁和谁在一起。如果有多个构造方法,使用@Inject注解,系统如何知道该用哪个构造方法来初始化@Inject标记的注解呢?@Qualifier就是结婚证,指定了哪个对象该用哪个构造方法来初始化。

  • Student中有多个构造方法
public class Student {
    private String name;

    public Student() {
        this.name = "test default";
        System.out.println(this.name);
    }

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

    public String getName() {
        return this.name;
    }
}
  • 媒婆
@Component(modules = TestModule.class)
public interface TestComponent {
    void inject(TestActivity activity);
}
  • 锄头
@Module
public class TestModule {
    private String name;

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

    @ChoiceInstance
    @Provides
    public Student provideStu() {
        return new Student();
    }

    @Provides
    public String provideName() {
        return name;
    }

    @ChoiceInstance(choiceType = ChoiceInstance.ONE_PARAMS)
    @Provides
    public Student provideStudent(String name) {
        return new Student(name);
    }
}
  • 调用
public class TestActivity extends Activity {
    @BindView(R.id.btn_test)
    public Button btn;

    @ChoiceInstance
    @Inject
    Student mStudent;

    @ChoiceInstance(choiceType = ChoiceInstance.ONE_PARAMS)
    @Inject
    Student oneStu;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
        ButterKnife.bind(TestActivity.this);

        DaggerTestComponent.builder().testModule(new TestModule("yds")).build().inject(this);

    }

    @OnClick(R.id.btn_test)
    public void clickTest() {
        Toast.makeText(TestActivity.this, oneStu.getName(), Toast.LENGTH_SHORT).show();
    }
}

3.4 坑

如果一个Activity中同时调用两个不同依赖类会怎么样?如在TestActivity中同时使用Teacher和Student的实例会怎么样?调用方法如下:

public class TestActivity extends Activity {
    @BindView(R.id.btn_test)
    public Button btn;

    @Inject
    Teacher mTeacher;

    @ChoiceInstance
    @Inject
    Student mStudent;

    @ChoiceInstance(choiceType = ChoiceInstance.ONE_PARAMS)
    @Inject
    Student oneStu;
	......
}

如果像上述那样调用,你通过ctrl+F9来构建,会报如下错误:

错误: [Dagger/MissingBinding] @com.crystallake.cod.itf.ChoiceInstance(choiceType=0) com.crystallake.cod.test.Student cannot be provided without an @Provides-annotated method.
@com.crystallake.cod.itf.ChoiceInstance com.crystallake.cod.test.Student is injected at
com.crystallake.cod.test.TestActivity.mStudent
com.crystallake.cod.test.TestActivity is injected at
com.crystallake.cod.component.TeacherComponent.inject(com.crystallake.cod.test.TestActivity)

在这里插入图片描述
你会诧异,TeacherComponent怎么报的错误跟Student有关?其实这时候虽然报错,但还是会生成DaggerXXXComponent文件,这里生成的是DaggerTestComponent文件,可以看到:

  ......
  private TestActivity injectTestActivity(TestActivity instance) {
    TestActivity_MembersInjector.injectMTeacher(instance, new Teacher());
    TestActivity_MembersInjector.injectMStudent(instance, TestModule_ProvideStuFactory.provideStu(testModule));
    TestActivity_MembersInjector.injectOneStu(instance, getChoiceInstanceStudent());
    return instance;
  }
  ......

可以看到这里生成了DaggerTestComponent而没有生成DaggerTeacherComponent,而且Teacher的Inject初始化也在DaggerTestComponent中,完全和Student混合在了一起。那么该怎么解决呢?可以让@Component同时依赖TeacherModule和TestModule。如下所示:

@Component(modules = {TestModule.class, TeacherModule.class})
public interface TestComponent {
    void inject(TestActivity activity);
}

然后删除掉TeacherComponent并增加TeacherModule。TeacherModule如下所示:

@Module
public class TeacherModule {

    @Provides
    public Teacher provideTeacher(){
        return new Teacher();
    }
}

总结:如果同一个类中同时需要注解多个类,则可在同一个@Component中同时依赖多个@Module。


参考文章:


  1. 秒懂依赖注入及 Dagger2 的实用技能(如何在Android中使用) ↩︎

  2. Android Dagger2 从零单排(一) 基础注解 ↩︎

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值