专栏名称: Android博客周刊
[ Android Blog 周刊 ]每周一准时更新,主要包括本周最新的优秀国内外博客,新闻,类库,视频等 [www.androidblog.cn ] [ QQ群:149581646 ]
目录
相关文章推荐
开发者全社区  ·  43岁冰冰显老了 ·  昨天  
开发者全社区  ·  立省110亿!百度完成重大收购 ·  昨天  
开发者全社区  ·  De­e­p­S­e­ek创始人梁文锋疑似匿 ... ·  昨天  
开发者全社区  ·  凌晨五点通知裁员 ·  2 天前  
开发者全社区  ·  麻了!阿里P7炫总包超150w ·  2 天前  
51好读  ›  专栏  ›  Android博客周刊

Dagger2 入门,以初学者角度

Android博客周刊  · 公众号  · android  · 2017-01-13 08:17

正文

温馨提示:

微信公众号做了超链接限制,有兴趣的小伙伴可以直接到

www.androidblog.cn

或点击文章末尾" 阅读全文 "里进行查看

注意 【招编辑和分享讲师,有意者公众号留言】



作者简介:

本文作者 Fxiang_

本文原地址: http://www.jianshu.com/p/1d84ba23f4d2

文章源自网络,如果涉及侵权等问题,请第一时间联系我们予以下架



依赖注入


Dagger2是Android中比较热门的依赖注入框架,什么是依赖注入呢?维基百科上是这样描述的:


控制反转(Inversion of Control,缩写为IoC),是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入(Dependency Injection,简称DI),还有一种方式叫“依赖查找”(Dependency Lookup)。通过控制反转,对象在被创建的时候,由一个调控系统内所有对象的外界实体,将其所依赖的对象的引用传递给它。也可以说,依赖被注入到对象中.


通俗的来讲呢,就是一个类中需要依赖其他对象时,不需要你亲自为那些需要依赖的对象赋值,为那些对象赋值的操作交给了IOC框架.


Dagger2介绍


一般的IOC框架都是通过反射来实现的,但Dagger2作为Android端的IOC框架,为了不影响性能,它是通过apt动态生成代码来实现的.


Dagger2主要分为三个模块:


  1. 依赖提供方Module,负责提供依赖中所需要的对象,实际编码中类似于工厂类

  2. 依赖需求方实例,它声明依赖对象,它在实际编码中对应业务类,例如Activity,当你在Activity中需要某个对象时,你只要在其中声明就行,声明的方法在下面会讲到.

  3. 依赖注入组件Component,负责将对象注入到依赖需求方,它在实际编码中是一个接口,编译时Dagger2会自动为它生成一个实现类.


Dagger2的主要工作流程分为以下几步:


  1. 将依赖需求方实例传入给Component实现类

  2. Component实现类根据依赖需求方实例中依赖声明,来确定该实例需要依赖哪些对象

  3. 确定依赖对象后,Component会在与自己关联的Module类中查找有没有提供这些依赖对象的方法,有的话就将Module类中提供的对象设置到依赖需求方实例中


通俗上来讲就好比你现在需要一件衣服,自己做太麻烦了,你就去商店买,你跟商店老板说明你想要购买的类型后,商店老板就会在自己的衣服供应商中查找有没有你所说的类型,有就将它卖给你.其中你就对应上面所说的依赖需求方实例,你只要说明你需要什么,商店老板则对应Component实现类,负责满足别人的需求,而衣服供应商则对应Module类,他负责生产衣服.也许这里有点绕,但经过下面的Demo,也许能够帮助你理解.


书写Demo


引入Dagger2


在项目下的build.gradle文件中添加apt插件:


buildscript {
    ...
    dependencies {
        classpath 'com.android.tools.build:gradle:2.2.2'       
 // NOTE: Do not place your application dependencies here; they belong        // in the individual module build.gradle files        //添加apt插件        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'    } } ...


在app目录的build.gradle文件中添加:


//应用apt插件apply plugin: 'com.neenbedankt.android-apt'

...

dependencies {
    ...    //引入dagger2
    compile 'com.google.dagger:dagger:2.4'
    apt 'com.google.dagger:dagger-compiler:2.4'    //java注解
    provided 'org.glassfish:javax.annotation:10.0-b28'
}


编写布料类Cloth


写一个Cloth类用作依赖对象,它包含一个color属性

public class Cloth {    
   private String color;    
   public String getColor() {      
        return color;    }    
    
    public void setColor(String color) {        
        this.color = color;    }    
        
    @Override    public String toString() {      
     return color + "布料";    }
      }


书写Module类


现在的需求是MainActivity中需要使用到Cloth对象,所以我们要为MainActivity书写一个Module类用来提供Cloth对象,相当于创建了一个提供商


@Modulepublic class MainModule {   

    @Provides    public Cloth getCloth() {        Cloth cloth = new Cloth();        cloth.setColor("红色");        
    return cloth;    } }


嗯?怎么多了两个注解?这两个注解有什么用呢?
注解是Dagger2中的关键,编写Module类时要在该类上声明 @Module 以表明该类是Module类,这样Dagger2才能识别,那 @Provides 又是干嘛的呢?它的作用是声明Module类中哪些方法是用来提供依赖对象的,当Component类需要依赖对象时,他就会根据返回值的类型来在有 @Provides 注解的方法中选择调用哪个方法.在一个方法上声明 @Provides 注解,就相当于创建了一条生产线,这条生产线的产物就是方法的返回值类型.有了这条生产线,供应商就能提供这种类型的商品了,当商店老板发现有人需要这种类型的商品时,供应商就可以提供给他了


书写Component接口

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


和Module类一样,Component类也是需要注解声明的,那个注解就是 @Component ,但是 @Component 注解的作用可不是单单用来声明Component类,他还有更强大的功能, @Component 注解有modules和dependencies两个属性,这两个属性的类型都是Class数组,modules的作用就是声明该Component含有哪几个Module,当Component需要某个依赖对象时,就会通过这些Module类中对应的方法获取依赖对象,MainComponent中只包含MainModule,所以令modules=MainModule.class,相当于供应商和商店老板确定合作关系的合同.而dependencies属性则是声明Component类的依赖关系,这个下面再详讲.


接口中那个方法又是干嘛用的呢?
我们现在只是声明了Component类,但我们要怎么将Component类和依赖需求方对象联合起来呢?答案就是通过这个inject方法,这个方法可以将依赖需求方对象送到Component类中,Component类就会根据依赖需求方对象中声明的依赖关系来注入依赖需求方对象中所需要的对象,本Demo中MainActivity中需要Cloth对象,所以我们通过inject方法将MainActivity实例传入到MainComponent中,MainComponent就会从MainModule中的getCloth方法获取Cloth实例,并将该实例赋值给MainActivity中的cloth字段.相当于你去商店的道路,没有这条路,你就无法去商店和老板说明你所需要的东西. 但是这里需要注意的是,inject方法的参数不能用子类来接收,例如本Demo中,如果inject的参数是Activity,那么Dagger2就会报错.


在MainActivity中声明

public class MainActivity extends AppCompatActivity {   
 private TextView tv;  
 
 
  @Inject
  Cloth cloth;
      
   @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        tv = (TextView) findViewById(R.id.tv);        MainComponent build = DaggerMainComponent.builder().mainModule(new MainModule()).build();        build.inject(this);        tv.setText("我现在有" + cloth);    } }


上面代码中有两处关键:


  1. 声明依赖对象Cloth,就是在cloth字段上添加 @Inject 注解,Dagger2中声明依赖对象都是通过 @Inject 注解, 但是 @Inject 注解的字段不能是private和protected的.


  2. 通过Dagger2自动生成的类来创建Component的实现类,创建时需要传入该Component实现类所需要的Module类实例,传入方法就是调用Module类类名首字母小写对应的方法.这里我们通过Dagger2自动生成的DaggerMainComponent类创建了MainComponent的实例,相当于我们创建了一个实实在在的商店,不再是理论上的商店,但是创建商店一定也要创建真实的供应商嘛,所以创建Component实现类时一定要传入Module的实例.(注意编写完Component接口后Dagger2并不会自动创建对应的类,需要我们点击Android Studio中bulid菜单下的Rebulid Poject选项,或者直接书写代码,编译时Dagger2就会帮你自动生成).
    再将MainActivity通过inject方法发送到MainComponent中,调用完inject方法后,你就会发现,MainActivity中的cloth字段已经被赋值,而且该cloth对应的就是我们在MainModule类getCloth方法中创建的Cloth对象.



结果





另一种方法


前面的Demo可能给人最大的感受就是麻烦吧?就是为cloth赋个值,又要写什么Module类,又是要写什么Component接口.其实Dagger2还可以用注解来提供依赖对象.让我们来瞧瞧怎么使用.


创建依赖类Shoe

我们又创建一个依赖类Shoe

public class Shoe {   

   @Inject    public Shoe() {
       }    

 @Override    public String toString() {        return "鞋子";    } }


但是这次我们创建的方式和Cloth不一样了,我们在构造函数上声明了 @Inject 注解,这个注解有什么用呢?作用可大了,当Component在所拥有的Module类中找不到依赖需求方需要类型的提供方法时,Dagger2就会检查该需要类型的有没有用 @Inject 声明的构造方法,有则用该构造方法创建一个.


相当于你去商店购买东西,你需要的东西商店的供应商不生产,商店老板就只好帮你去网上看看有没有你需要的东西,有则帮你网购一个.(假设你不会网购,哈哈^ ^).


在MainActivity中声明Shoe依赖

我们修改之前的MainActivity,添加一点东西


public class MainActivity extends AppCompatActivity {
    ...    
  @Inject    Shoe shoe;  

   
   @Override    protected void onCreate(Bundle savedInstanceState) {          ...        tv.setText("我现在有" + cloth + "和" + shoe);    } }

结果



注意:

有些读者可能会这样想:为什么不都用这种方法来声明呢?为什么要用Module类?
答案是这样的,项目中我们会用到别人的jar包,我们无法修改别人的源码,就更别说在人家的类上添加注解了,所以我们只能通过Module类来提供.


复杂一点的情况


我们创建的这些依赖类都不用依赖于其它依赖类,但是如果需要依赖于其它依赖类又要怎么弄呢?


创建依赖类Clothes

我们又来创建一个衣服类Clothes,制作衣服时需要布料,所以我们在创建Clothes的实例时需要用到Cloth实例


public class Clothes {   

 private Cloth cloth;    
 
     public Clothes(Cloth cloth) {      
          this.cloth = cloth;    }  
  
   public Cloth getCloth() {      
         return cloth;    }  
         
  @Override    public String toString() {        return cloth.getColor() + "衣服";    } }


在Module类中增加提供方法


现在我们的MainActivity中需要依赖于Clothes对象,所以我们在MianModule中添加提供Clothes对象的方法,但是Clothes需要依赖于Cloth对象,这要怎么办呢?可能最先想到的办法就是这样:


    @Provides
    public Clothes getClothes(){
        Cloth cloth = new Cloth();
        cloth.setColor("红色");        return new Clothes(cloth);
    }


直接在方法中创建一个Cloth不就得了,但是你有没有发现,创建Cloth的代码已经在getCloth方法中有了,我们能不能用getCloth方法中创建的Cloth实例来创建Clothes实例呢?


Dagger2提供了这样的功能,我们只要在getClothes方法中添加Cloth参数,Dagger2就会像帮依赖需求方找依赖对象一样帮你找到该方法依赖的Cloth实例,所以我们代码可以这样改:


    @Provides
    public Clothes getClothes(Cloth cloth){        return new Clothes(cloth);
    }


在MainActivity中声明Clothes依赖

我们修改之前的MainActivity,添加一点东西


public class MainActivity extends AppCompatActivity {
    ...    

   
   
   @Inject    Clothes clothes;  

    @Override    protected void onCreate(Bundle savedInstanceState) {        ...        tv.setText("我现在有" + cloth + "和" + shoe + "和" + clothes);    } }

结果



依赖总结


同理,在带有 @Inject 注解的构造函数要是依赖于其它对象,Dagger2也会帮你自动注入.笔者就不测试了,希望读者亲测一下.


这里我们引用 依赖注入神器:Dagger2详解系列 中的一段话:


我们有两种方式可以提供依赖,一个是注解了@Inject的构造方法,一个是在Module里提供的依赖,那么Dagger2是怎么选择依赖提供的呢,规则是这样的:


  • 步骤1:查找Module中是否存在创建该类的方法。

  • 步骤2:若存在创建类方法,查看该方法是否存在参数

  • 步骤2.1:若存在参数,则按从步骤1开始依次初始化每个参数

  • 步骤2.2:若不存在参数,则直接初始化该类实例,一次依赖注入到此结束

  • 步骤3:若不存在创建类方法,则查找Inject注解的构造函数,看构造函数是否存在参数

  • 步骤3.1:若存在参数,则从步骤1开始依次初始化每个参数

  • 步骤3.2:若不存在参数,则直接初始化该类实例,一次依赖注入到此结束


也就说Dagger2会递归的提供依赖.




@Named和@Qulifier注解的使用

@Named


假设我们现在又有了新的需求,MainActivity中需要两种布料,分别是红布料和蓝布料,但我们的MainModule类中只能提供红布料,怎么办呢?
读者可能会想:在MainModule类中再添加一个提供蓝布料的方法不就行了:


    @Provides
    public Cloth getRedCloth() {
        Cloth cloth = new Cloth();
        cloth.setColor("红色");       
        return cloth;    }    

   @Provides    public Cloth getBlueCloth() {        Cloth cloth = new Cloth();        cloth.setColor("蓝色");        
       return cloth;    }


可问题就来了,Dagger2是通过返回值类型来确定的,当你需要红布料时,它又怎么知道哪个是红布料呢?所以Dagger2为我们提供 @Named 注解,它怎么使用呢?它有一个value值,用来标识这个方法是给谁用的.修改我们的代码:


    @Provides
    @Named("red")   
  public Cloth getRedCloth() {        Cloth cloth = new Cloth();        cloth.setColor("红色");        
         return cloth;    }    
         
    @Provides    @Named("blue")    
    public Cloth getBlueCloth() {        Cloth cloth = new Cloth();        cloth.setColor("蓝色");      
         return cloth;    }


我们在getRedCloth方法上使用 @Named("red") 表明此方法返回的是红布料,同理,在getBlueCloth方法上使用 @Named("blue") 表明此方法返回的是蓝布料,接下我们只要在MainActivity中的布料字段上同样使用 @Named 注解,就可以一一配对了.


public class MainActivity extends AppCompatActivity {
    ...    
   @Inject    @Named("red")    Cloth redCloth;    
   
   @Inject    @Named("blue")    Cloth blueCloth;    
   
   
   @Override    protected void onCreate(Bundle savedInstanceState) {        ...        tv.setText("我现在有" + redCloth + "和" + blueCloth );    } }

redCloth 上用 @Named("red") 标记后,他就会对应Module中对应的方法.

结果





@Qulifier

@Qulifier 功能和 @Named 一样,并且 @Named 就是继承 @Qulifier 的,我们要怎么使用 @Qulifier 注解呢?答案就是自定义一个注解:


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


有了这个注解,我们就可以用它在替换掉上面的 @Named("red") ,效果是一样的.读者可以亲自试一试.


而且这两个注解还能使用在依赖参数上,比如这个:


    @Provides
    public Clothes getClothes(@Named("blue") Cloth cloth){        return new Clothes(cloth);
    }

效果和上面说明的一样,进入这个方法的cloth由上面有 @Named("blue") 的方法提供



@Singleton和@Scope的使用

@Singleton


假设现在MainActivity中需要依赖Clothes和Cloth,我们在MainModule中提供这两个类的提供方法:


@Modulepublic class MainModule {    
   
   @Provides    public Cloth getRedCloth() {        Cloth cloth = new Cloth();        cloth.setColor("红色");        
       return cloth;    }  
       
    @Provides    public Clothes getClothes(Cloth cloth){        
        
        return new Clothes(cloth);    } }


接着在MainActivity中声明:

public class MainActivity extends AppCompatActivity {   

 private TextView tv;    
 
     @Inject    Cloth redCloth;    
     
     @Inject    Clothes clothes;  
     
      @Override    protected void onCreate(Bundle savedInstanceState) {        
      super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        tv = (TextView) findViewById(R.id.tv);        MainComponent build = DaggerMainComponent.builder().mainModule(new MainModule()).build();        build.inject(this);        tv.setText("redCloth=clothes中的cloth吗?:" + (redCloth == clothes.getCloth()));    } }

运行结果:




你会发现,MainActivity中的Cloth对象和Clothes中的Cloth对象并不是同一个对象,注入过程中,对cloth注入时会调用一次getRedCloth方法,创建了一个Cloth对象;注入Clothes时又会调用一次getRedCloth方法,这时又会创建一个Cloth对象,所以才会出现上面的结果.但是如果需要MainActivity中的Cloth对象和Clothes中的Cloth对象是同一个对象又要怎么办呢?Dagger2为我们提供了 @Singleton 注解,和名字一样,这个注解的作用就是声明单例模式,我们先看看它怎么使用,下面再讲原理.


首先,在getRedCloth方法上添加该注解:


    @Singleton
    @Provides
    public Cloth getRedCloth() {
        Cloth cloth = new Cloth();
        cloth.setColor("红色");        
       return cloth;    }

再在MainComponent接口上添加该注解:

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

我们看看运行结果:


dagger2demo_6.png


有没有发现,MainActivity中的Cloth对象和Clothes中的Cloth对象是同一个对象了,是不是很神奇!


@Scope

@Singleton 是怎么实现的呢?我们先看看 @Scope 注解,弄懂它, @Singleton 你也就会明白了,下面我们就来分析分析
顾名思义, @Scope 就是用来声明作用范围的. @Scope @Qulifier 一样,需要我们自定义注解才能使用,我们先自定义一个注解:

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


这个注解有什么用呢?答案就是声明作用范围,当我们将这个注解使用在Module类中的Provide方法上时,就是声明这个Provide方法是在PerActivity作用范围内的,并且当一个Component要引用这个Module时,必须也要声明这个Component是PerActivity作用范围内的,否则就会报错,声明方法也很简单,就是在Component接口上使用这个注解.但是我们声明这个作用范围又有什么用呢?原来Dagger2有这样一个机制:在同一个作用范围内,Provide方法提供的依赖对象就会变成单例,也就是说依赖需求方不管依赖几次Provide方法提供的依赖对象,Dagger2都只会调用一次这个方法.就和上面那个例子一样,正常情况下,在注入MainActivity中的Cloth对象时会调用一次getRedCloth方法,注入Clothes对象时因为依赖Cloth对象,所以又会调用一次getRedCloth方法,导致这两个Cloth对象并不是同一个实例.但是我们给它声明作用范围后,这两次对Cloth的依赖只会调用一次getRedCloth方法,这样这两个Cloth对象就是同一实例了,这样就保证了在给MainActivity注入时,所有声明的Cloth依赖都是指向同一个实例.(注意:只有Module类中声明了作用范围的Provide方法才能实现单例,没声明的方法就不是单例的)查看源码你会发现 Singleton 其实是继承 @Scope 注解的,所以你知道了 Singleton 是怎么实现单例模式的吧.


可能有些读者可能会问,Dagger2既然有了 Singleton 为什么还要我们自定义 PerActivity 注解?这就涉及到代码可读性了,当依赖需求方是Activity时,我们可以自定义一个 PerActivity 注解,当依赖需求方是Fragment时,我们又可以自定义一个 PerFragment 注解,这样我们就能清楚的区分依赖对象的提供目标了


那我们通过构造函数提供依赖的方式又要怎么声明作用范围呢?答案就是在类名上使用注解标明, 切记不要在构造函数上用注解标明,这样是无效的 .


读者可以试试用 PerActivity 注解代替上面例子中的 Singleton 注解,你会发现效果是一样的


注意注意注意 : 单例是在同一个Component实例提供依赖的前提下才有效的,不同的Component实例只能通过Component依赖才能实现单例.也就是说,你虽然在两个Component接口上都添加了 PerActivity 注解,但是这两个Component提供依赖时是没有联系的,他们只能在各自的范围内实现单例.(下一个例子会体现到)



总结:


  • 至此,Dagger2基础已讲完,对于Dagger2在项目中的使用方法,可以参考github上的开源项目.希望此篇文章能够对你有所帮助!

  • 本篇文章是笔者用来记录自己对Dagger2的理解的,如果当中有错误,还请赐教,以便笔者纠正.

  • 能够书写本篇文章,还得多亏了各位大神的blog,正因为各位大神的分享精神,才让我们这种小菜鸟能够成长.此篇文章分享出来的目的也就是为了传承这种精神


最后我们引用一下 Dagger2 Scope 注解能保证依赖在 component 生命周期内的单例性吗? 中的注意事项:


  • component 的 inject 函数不要声明基类参数;


  • Scope 注解必须用在 module 的 provide 方法上,否则并不能达到局部单例的效果;


  • 如果 module 的 provide 方法使用了 scope 注解,那么 component 就必须使用同一个注解,否则编译会失败;


  • 如果 module 的 provide 方法没有使用 scope 注解,那么 component 和 module 是否加注解都无关紧要,可以通过编译,但是没有局部单例效果;


  • 对于直接使用 @Inject 构造函数的依赖,如果把 scope 注解放到它的类上,而不是构造函数上,就能达到局部单例的效果了;


笔者再总结

  • 被依赖的Component能提供某个对象时,一定要在接口中声明以该对象为返回值的方法(也就是暴露接口).这样依赖它的Component才能获取到这种对象.


注:由于微信2W字限制,本文不太完整,如果你想有兴趣请点击阅读原文即可



------------------------------  End  ------------ --------------------


精选文章:

View事件体系 面试遇到的那些坑 Android密钥保护和C/S网络传输安全理论指南

随机洗牌算法 Android增量编译 由模块化到组件化(一) 一致性Hash算法

Android应用耗电量分析与优化建议 我不是不同意你的看法,而是受不了你说话的样子

仿京东手势解锁 开发人员一份技术简历写好很重要 泛型深度解耦下的MVP大瘦身

模板方法模式


往期周刊:

50期 | 49期 | 48期 | 47期 |







请到「今天看啥」查看全文