Android框架—Dagger2使用

一、概述

现在android开发越来越多的应用到各种框架,一个好的app架构不但可以支持好的性能,同时也可以更方便快捷的开发功能。依赖注入是java后端开发spring的核心技术,而对于android来说现在由谷歌爸爸亲管的dagger则是不二之选。

二、dagger环境配置

使用Android Studio 创建一个新的项目,在Project的 build.gradle文件添加以下内容:

buildscript {

    dependencies {
        // 顺便也配置了支持lambda表达式
        classpath 'me.tatarka:gradle-retrolambda:3.2.4' 
        // classpath 'com.android.tools.build:gradle:3.0.1' 正常gradle插件配置
    }
}

并在Module下的build.gradle添加以下内容:

apply plugin: 'me.tatarka.retrolambda'
android {
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}
dependencies {

    annotationProcessor 'com.google.dagger:dagger-compiler:2.4'
    compile 'com.google.dagger:dagger:2.4'
    provided 'org.glassfish:javax.annotation:10.0-b28'
}

注意:使用新插件时,必须使用 annotationProcessor 依赖项配置将注解处理器添加到处理器类路径中。

三、dagger的注解

dagger的基本使用主要是通过各项注解来实现的,所以搞清楚常用注解的作用十分重要

  • @Inject:有两个作用,1是在需要依赖的类(目标类,即宿主)中标记成员变量告诉Dagger这个类型的变量需要一个实例对象。2是标记类中的构造方法(一般为无参构造方法)告诉Dagger我可以提供这种类型的依赖实例。
  • @Provides:用来提供依赖实例,对方法进行注解,且都是有返回类型的。用来告诉Dagger我们想如何创建并提供该类型的依赖实例(一般会在方法中new出实例)。用@Provides标记的方法,谷歌推荐采用provide为前缀,必须用在@Module注解的类中,方法所需的参数也需要以方法的形式返回提供。
  • @Module:用来标记类(一般类名以Module结尾)。Module主要的作用是用来集中管理@Provides标记的方法。我们定义一个被@Module注解的类,Dagger就会知道在哪里找到依赖来满足创建类的实例。modules的一个重要特征是被设计成区块并可以组合在一起供@Component所注解的类使用。
  • @Component:用来标记接口或者抽象类(一般以Component结尾),是@Inject(指第一个作用)和@Module之间的桥梁,主要职责是把二者组合在一起,Module中的实例对象必须在Component中暴露出来才能供之后使用。所有的components都可以通过它的modules知道它所提供的依赖范围。一个Component可以依赖一个或多个Component,并拿到被依赖Component暴露出来的实例,Component的dependencies属性就是确定依赖关系的实现。
  • @Scope:作用域,Dagger2通过自定义注解来限定作用域,有一个默认的作用域注解@Singleton,通常在Android中用来标记在App整个生命周期内存活的实例。也可以自定义一个@PerActivity、@PerFragment注解,用来表明实例生命周期与Activity、Fragment一致。我们可以自定义作用域的粒度(比如@PerUser等等)。
  • @Qualifier:限定符。当一个类的类型不足以标示一个依赖的时候,我们就可以用这个注解。例如,我们有两个@Provide注解的方法都需要一个String参数,那么在提供依赖的方法上面就可以通过自定义标识“@ForData”或者“@ForImage”来进行区别Dagger2里面已经存在一个限定符@Named注解,通过@Named(”xxxxx”)就可以进行标识。具体使用请看下方,十分简单明了。
  • @SubComponent:如果我们需要父组件全部的提供对象,这时我们可以用包含方式而不是用依赖方式,相比于依赖方式,包含方式不需要父组件显式显露对象(依赖方式只能拿到暴露出的实例),就可以拿到父组件全部对象。且SubComponent只需要在父Component接口中声明就可以了。

四、dagger基本使用

1.通过@Inject、@Component实现依赖注入(最简单的方式):
提供一个依赖注入的对象,比如User

public class User {
    private String name;
    // 这个@Inject表示可以提供User类型的实例
    @Inject
    public User() {
        this.name = "this is test";
    }
    public String getName() {
        return name;
    }
}

提供一个宿主,比如Activity

public class MainActivity extends AppCompatActivity {

    @Inject
    User user;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

注意:需要注意的是使用@Inject注解的变量不能是private变量,否则会报以下错误:Error:(13, 20) 错误: Dagger does not support injection into private fields

提供一个桥梁,即Component

@Component()
public interface TestMainComponent {
    /**
     * 必须让Component知道需要往哪个类中注入
     * 这个方法名可以是其它的,但是推荐用inject
     * 目标类MainActivity必须精确,不能用它的父类
     */
    void inject(MainActivity activity);
}

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

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

到此最简单的dagger使用已经完成了,但是用@Inject提供实例有一定的缺陷:

  • @Inject注解的构造方法有参数的话,参数也需要有其他地方提供依赖
  • @Inject注解了一个对象的两个构造方法就会报错,dagger不知道用哪个进行实例化
  • 对于第三方库或者一些我们无法修改构造方法的对象无法使用@Inject

此时,就要使用@Provides和@Module配合提供实例。

2.通过@Provides、@Module提供依赖
改造上面的User构造函数

public class User {
    private String name;
    // 这个@Inject表示可以提供User类型的实例
    public User(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
}

创建一个Module来管理@Provides注解的方法

@Module
public class MainModule {

    @Provides
    @Singleton
    User provideUser() {
        return new User("test");
    }
}

改造上面的Component

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

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

DaggerTestMainComponent.builder().mainModule(new MainModule()).build().inject(this);
// 如果MainModule只有一个无参构造方法,可用如下初始化
DaggerTestMainComponent.create().inject(this);

注意:在提供依赖对象这一层面上,@Provides级别高于@Inject。(具体可以测试证明,这里不作说明)

3.初始化依赖实例的步骤

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

注意:
1.既没有@Module提供的实例,也没有@Inject标记的构造方法编译期就会报错。
2.若实例化时需要的参数没有提供,则实例化后值为null

五、dagger的辅助注解

上面的@Inject、@Component、@Module、@Provides是dagger实现依赖注入的主要工具,另外的@Qualifier和@Scope则起到了辅助作用

1.@Qualifier限定符的作用
Dagger2中已经有一个定义好的限定符@Named:

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

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

同时我们也可以自定义@ForTest这样的限定符

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

还是以上面的为例,改造MainModule

@Module
public class MainModule {

    @Provides
    @Named("name")
    User provideUser() {
        return new User("user");
    }


    @Provides
    @ForTest
    User provideUser1() {
        return new User("user1");
    }

    @Provides
    String provideUserName(@ForTest User user) {
        return user.getName();
    }
}

此处provideUserName方法需要一个User参数,通过@Qualifier指定所需要的那个实例

在宿主中使用时

    @Inject
    @Named("name")
    User user;

    @Inject
    @ForTest
    User user1;

使用时也可以通过@Qualifier来选择所需要的那个实例

2.@Scope作用域的使用
个人认为@Scope的作用主要是在组织Component和Module的时候起到一个实例作用范围的提醒(类似是生命周期),Dagger2中有一个默认的作用域@Singleton:

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

@Singleton并不是我们传统单例模式的那种作用,并不能直接帮我们创建单例对象,但它通常用于整个的应用全局对象,配合android中application的特殊性,就可以实现单例(此处不讨论多进程)

改造上面的MainModule

@Module
public class MainModule {

    @Provides
    @Singleton
    User provideUser() {
        return new User("test");
    }
}

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

所以需要改造上面的Component

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

然后在自定义的Application中实例化Component

public static TestMainComponent component;

public static AppComponent getAppComponent(){
        if (component == null) {
            component = DaggerAppComponent.builder()
                    .mainModule(new MainModule())
                    .build();
        }
        return component;
    }

这样就实现了一个常规意思上的单例,但是可以发现这个依赖实例其实只是每次都由同一个Component注入器对象提供,重新生成一个Component对象的话注入的依赖实例就不再是同一个。所以想要用Component只提供同一个实例对象,就必须保证Component只初始化一次。

我们可以自定义作用域,比如@PerActivity、@PerFragment设置@PerUser

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

使用方法和@Singleton一样,但具体的差别需要在实际项目中测试体会,后续有机会再具体解释(可用来控制对象的生命周期,快捷实现一些常规功能)

六、组织Component

通过上面可以发现dagger的基本使用十分简单,但难点其实在于合理的组织component,一个应用,多个activity,多个fragment,多个数据对象等。

Component组织方式分为3种:
- 依赖方式:一个Component可以依赖一个或多个Component,采用的是@Component的dependencies属性。
- 包含方式:这种就需要@SubComponent注解,用@SubComponent标记接口或者抽象类,表示它可以被包含。一个Component可以包含一个或多个Component,而且被包含的Component还可以继续包含其他的Component(跟Activity包含Fragment方式类似)
- 继承方式:——用一个Component继承另外一个Component。

下面借用其他项目的组织方式Android-CleanArchitecture
这里写图片描述

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

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

个人觉得,实际开发中用@Singleton标记来表示在App生命周期内全局的对象,然后用自定义的@PerActivity、@PerFragment等来表示跟Activity、Fragment生命周期一致比较好。

现在我们采用依赖、包含、继承的方式来演示Component的组织方式

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中暴露出去。(直接使用的话不需要)这个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;
    }
}

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

有一个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:

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

初始化注入是这个样子:

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

上面的ActSubComponent 是被包含,它需要有个@Subcomponent注解,如果是包含的方式,作用域可以与包含它的Component一致。(2.8版本开始好像会报错)

@Subcomponent
@PerAct // 作用域与上一层的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);

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
@PerActivity
@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);

七、总结

通过以上至少可以了解Dagger2中常用方式,这边提一下注意点:

  • @Provides提供依赖的优先级高于@Inject
  • 实例化对象需要的参数没有提供时会为null
  • @Singleton并不是真的能创建单例
  • Component的作用域必须与对应的Module作用域一致,如果@Module没有标记作用域,就不影响。
  • Component和依赖的Component作用域范围不能一样。
  • 由于@Inject,@Module和@Provides注解是分别验证的,所有绑定关系的有效性是在@Component层级验证。

Dagger的使用远远不仅于此,还有很多可以优化加深的地方,以后有机会再记录,接下来会分享下之前项目中实际应用的方式。(本文内容都是个人理解与网络借鉴,有错误和遗漏之处,欢迎指正,共同学习。)

展开阅读全文

没有更多推荐了,返回首页