依赖注入框架--Dagger2

依赖注入(DI, Dependency Injection),目的是为了解决类之间的耦合,方便测试。

减少耦合的一个原则是,一个类A需要的类B的对象的实例化不应该在A中实现,而是通过一定的方式从外部传入B的实例,这样就解决了类AB之间的耦合。

(PS:写这篇文章的目的不是说自己已经掌握了Dagger2,其实还有很多没有深刻理解的地方。把自己研究这几天的成果写出来,给想了解Dagger2的同学做个参考,同时也作为自己的一个备忘。有错误的地方,请大家斧正。)

举个例子:

public class ClassA {
    public ClassA() {
        //do some init work
    }
}

public class ClassB {
    
    private ClassA mInstanceA;

    public ClassB() {
        //ClassA的实例化在ClassB的构造方法中执行
        instanceA = new ClassA();
    }
}

如上,ClassB依赖于ClassA,并且在ClassB的构造方法中完成了ClassA的实例化。这样看上去似乎没有什么问题,但是当CLassA的构造方法改变,则必须要修改ClassB的代码。这时ClassAClassB之间有耦合,牵一发动全身。

依赖注入可以很好的解决这个问题。有很多种注入依赖的方法:

1.通过接口注入依赖。

重构一下ClassB的写法:

public interface IClassB {
        
    void setClassA(ClassA a);

}
public class ClassB implements IClassB {
    
    private ClassA mInstanceA;
    
    @Override
    void setClassA(ClassA a) {
        mInstanceA = a;
    }
    
}

2.通过setter方法注入依赖。

public class ClassB {
    
    private ClassA mInstanceA;
    
    public void setInstanceA(ClassA a) {
        mInstanceA = a;
    }
    
}

3.通过构造方法注入依赖。

public class ClassB {
    
    private ClassA mInstanceA;
    
    public ClassB(ClassA a) {
        mInstanceA = a;
    }
    
}

4.通过依赖注入框架注入依赖。

开始讲这篇文章的主角--Dagger2。

Dagger2是目前Android上比较流行的一个依赖注入框架,Google出品。之前在项目中@刘青也用到了这个框架,非常适合与MVP架构配合使用。

大家知道在MVP架构中,View持有Presenter的引用,Presenter持有Model的引用,其实说白了就是依赖。使用Dagger2可以很方便的注入这些依赖。

贴出部分代码:

public class LoginFragment extends BaseFragment implements LoginView {
    
    @Inject 
    LoginPresenter mLoginPresenter;
    
    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        injectDependencies();
    }
    
    private void injectDependencies() {
        DaggerLoginComponent.builder()
                .appComponent(ComponentHolder.getAppComponent())
                .loginModule(ModuleProvider.getInstance().provideLoginModule(this, getActivity()))
                .build()
                .inject(this);
    }

}

可以看出,LoginFragment依赖于LoginPresenter。在mLoginPresenter上添加@Inject注解,然后在onCreate方法中做了某种操作后,LoginPresenter就被注入到LoginFragment中了,具体是哪种操作,我们往下看。

首先看下Dagger2中常见的几个注解:

1.@Inject

在字段或构造方法前加上这个注解,表明这个字段需要被注入依赖或者标注依赖对应的构造方法。

2.@Module

@Module标注的类作为依赖的提供者。

3.@Provide

在方法前加上这个注解,表明这个方法提供依赖,通常用在被@Module注解的类的方法上。举个例子:

@Provides
InjectEntity provideInjectEntity() {
    return new InjectEntity();
}

表明这个方法提供InjectEntity的依赖。

@Component

Component是一个注入器。@Inject需要注入,@Module提供注入,Component就是这两者之间的桥梁。被@Component注解的一定是接口。

@Scope

@Scope可以自定义注解,通过自定义注解可以限定注解的作用域。

@Qualifier

当类的类型不足以鉴别一个依赖的时候,我们就可以使用这个注解标示。

代码

拿Google官方的demo来讲:

@Module
public class AndroidModule {
  private final DemoApplication application;

  public AndroidModule(DemoApplication application) {
    this.application = application;
  }

  @Provides 
  @Singleton
  @ForApplication
  Context provideApplicationContext() {
    return application;
  }

  @Provides 
  @Singleton
  LocationManager provideLocationManager() {
    return (LocationManager) application.getSystemService(LOCATION_SERVICE);
  }
}

这是一个Module,用来提供依赖的。看provideApplicationContext方法,@Provides注解表明它提供Context的依赖。当某个Context类型的字段被@Inject注解时,Dagger2会到Module中寻找返回值为Context,并且被@Provides标注的方法,通过这个方法将依赖注入到需要的类中。

看到这里你可能会有疑问,如果一个Module中有多个方法被@Provides标注,并且返回值是Context类型时,该如何选择?这时就需要用到@Qualifier标注了,先看下@ForApplication的具体写法:

@Qualifier @Retention(RUNTIME)
public @interface ForApplication {
}

在提供依赖的方法以及需要依赖注入的字段前都用@ForApplication标注,这两个就构成了一一对应关系,如果Module中还有一个返回值为Context的方法,但是没有用@ForApplication标注,就可以将其排除。

@ForApplication只是一个标记,你可以改成别的名字,只要能区别开其他有相同返回值的方法即可。

如下是被依赖字段的@Qualifier写法:

@Named("ForApplication") 
@Provides
Person provideContext(Context context) {
    return new Person(context);
}
另一种写法

其实还有一种简单的,不用定义新注解的写法:

@Named("Context")
@Provides
Person provideApplicationContext() {
 
    return new application;
    
}
@Named("Context")
@Inject
Context context;

在提供依赖的方法以及需要依赖注入的字段前都加上@Named标注,只要@Name里面的字符串相同,则构成一一对应的关系。这种写法会比较简便,但是字符串的标记容易导致不匹配,字符串写错了是不会报错的。

再看@Singleton注解。这是Dagger2自带的一个注解,用于保持在同一个Component中的对象是单例的。其实你去看@Singleton的源码,就简单的几行:

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

事实上,如果你自己定义一个注解,把Singleton名字换掉,效果和@Singleton是一样的。

可以下这样一个结论:

只要Component上添加的@Scope注解和Module中提供依赖的方法上添加的@Scope注解一样,就会保持这个方法返回的对象在该Component实例中是单例的。

这也就是很多使用Dagger2的例子中使用的@PerApp@PerActivity的来历。

public class DemoApplication extends Application {
  
  @Singleton
  @Component(modules = AndroidModule.class)
  public interface ApplicationComponent {
    void inject(DemoApplication application);
    void inject(HomeActivity homeActivity);
    void inject(DemoActivity demoActivity);
  }
  
  @Inject LocationManager locationManager;
  
  private ApplicationComponent component;

  @Override public void onCreate() {
    super.onCreate();
    component = DaggerDemoApplication_ApplicationComponent.builder()
        .androidModule(new AndroidModule(this))
        .build();
    component().inject(this);
  }

  public ApplicationComponent component() {
    return component;
  }
}

DemoApplication中有一个接口ApplicationComponent,用@Component注解表示这是一个Component,后面的(modules = AndroidModule.class)指明了与其相关联的Module

需要注意的是,Component必须是一个接口,而且需要引用到目标类的实例,上面代码中的inject()方法就是为Component提供目标类的,之所以这样是因为Component需要目标类的实例来寻找目标类中被@Inject标记的字段,以便为这些字段到Module找相应的提供依赖的方法。举个例子:

上述Component中有这个方法

void inject(DemoApplication application);

Component拿到DemoApplication的实例后会在其中找到下面这个字段:

 @Inject LocationManager locationManager;

然后,Component会到其对应的Module中去寻找用@Provides标记的,并且返回值是LocationManager的方法,通过找到的这个方法得到LocationManager的实例。到这里也就完成了依赖的注入。

继续看HomeActivity的写法:

public class HomeActivity extends DemoActivity {
  @Inject LocationManager locationManager;

  @Override protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ((DemoApplication) getApplication()).component().inject(this);

    Log.d("HomeActivity", locationManager.toString());
  }
}

继承自DemoActivity不用管,对理解Dagger没什么意义。

可以看到有一个被@Inject注解的字段locationManager,表示这个字段需要依赖注入。在onCreate中调用了Componentinject方法,传入目标类的引用。流程就像我们上面分析的那样,到Component到Module中寻找@Provides方法,不再赘述。

注意到之前AndroidModule中提供LocationManager依赖的方法是加入了Singletion依赖的,说明所提供的LocationManagerComponent中是单例的。我们可以再加一个LocationManager验证一下是否真的是单例(提示一下,这里有个坑,不要被我带进去了)。

HomeActivity代码改成如下所示:

public class HomeActivity extends DemoActivity {
  @Inject LocationManager locationManager;
  //新加一个相同类型的字段,用来验证是否真的是单例的
  @Inject LocationManager locationManager2;

  @Override protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ((DemoApplication) getApplication()).component().inject(this);

    Log.d("HomeActivity", locationManager.toString());
    Log.d("HomeActivity", locationManager2.toString());
  }
}

运行之后看下打印:

04-06 21:18:26.984 13517-13517/? I/HomeActivity: android.location.LocationManager@3d740cbf
04-06 21:18:26.984 13517-13517/? I/HomeActivity: android.location.LocationManager@3d740cbf

嗯,的确是一样的。那如果将Singletion去掉是不是应该就不一样了呢?去掉试下:

04-06 21:18:26.984 13517-13517/? I/HomeActivity: android.location.LocationManager@3d740cbf
04-06 21:18:26.984 13517-13517/? I/HomeActivity: android.location.LocationManager@3d740cbf

还是一样的?
额,只能说Google这个例子选的不好,为什么一定要用LocationManager这个类来举例子呢?翻上去看看provideLocationManager()这个方法咋写的:

getSystemService(LOCATION_SERVICE);

Android中的系统级别服务采用集合形式的单例模式,所以不管你调多少次,返回的都是同一个实例。其实可以在provideLocationManager()方法中加一个打印,你会发现加上@Singleton注解,provideLocationManager()只调用一次,去掉会调用两次,这样就符合我们预期了。

组件之间的依赖

看一个例子:

@Module
public class SubModule {
    
    @Provides
    public InjectEntity provideEntity(Context context) {
        return new InjectEntity();
    }
    
}

很显然,SubModule提供一个InjectEntity的依赖,但是提供依赖的同时provideEntity方法还依赖一个Context对象,如果在SubModule里有@Provides方法返回Context对象,就像下面这样:

@Module
public class SubModule {

    private Context mContext;

    public SubModule(Context context) {
        mContext = context;
    }
    
    @Provides
    public Context provideContext() {
        return mContext;
    }
    
    @Provides
    public InjectEntity provideEntity(Context context) {
        return new InjectEntity();
    }
    
}

这样provideEntity方法会从provideContext方法中获得Context对象的依赖。但是问题是如果SubModule中就是没有提供Context依赖的方法怎么办呢?这时会到其所依赖的Component中寻找:

@Component(modules = MainModule.class)
public Interface MainComponent {

    void inject(DemoApplication application);
    
}
@Module
public class MainModule {
    
    private Context mContext;
    
    public MainModule(Context context) {
        mContext = context;
    }
    
    @Provide
    public Context provideContext() {
        return mContext;
    }
    
}
@Component(dependencies = MainComponent.class, Module = SubModule.class)
public interface SubComponent {

    void inject(HomeActivity activity);
    
}

上面代码逻辑很清晰,SubComponent依赖于MainComponent,在SubModule中找不到的依赖会通过MainComponentMainModule中去寻找。

这里有一个需要注意的点:

一个没有作用域(unscoped )的组件不可以依赖有作用域的组件.

组件之间的依赖在Android中一般体现为ActivityFragment作用域的Component依赖于App作用域的Component,后者为前两者提供Application实例或一些需要在App全局使用的单例对象。


以上

参考:

google dagger2 官方参考文档:

https://google.github.io/dagger/

google官方demo地址:

https://github.com/google/dagger/tree/master/examples/android-simple

其他文章:

http://blog.csdn.net/lisdye2/article/details/51942511

http://www.jianshu.com/p/39d1df6c877d

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容