基于MVP模式的 dagger-android 探索

近期接手了公司一个项目的重构,要基于MVPArms框架来做,而arms又是基于dagger2构建;但是dagger2直接用于android的话用起来还是有些不太舒服,仗着自己对dagger有些了解,花了些时间把arms框架的dagger部分替换成了dagger-android。重构期间也遇到了一些问题,修修补补最后还算成功,对于dagger的理解也更深了一点;为了帮助自己记忆开始写这篇文章,同时也在这里分享给大家。

关于dagger2网上已经有了不少优秀的文章,我这里就直接从dagger-android开始讲起了,如果还对dagger2不太了解,建议先看看这个系列的文章Dagger2完全解析,个人认为非常不错。

1. 一些概念

首先我们来回顾一下dagger三要素

  • 目标类: 需要进行依赖注入的类,即依赖于一些其他类实例的类。

  • module: 准确地说应该是依赖,用于为目标类提供依赖。我们可以通过创建一个module手动new对象对外提供依赖,也可以通过@inject注解类的构造器让dagger自动创建对象提供依赖。

  • commponent: 翻译过来是组件的意思,在dagger中充当ioc容器。我们知道component可以依赖一个或多个module,然后它一般还需要有一个inject方法,类似这样:

    @Component(modules = {
            xxxModule.class,
            xxxModule.class
    })
    public interface AppComponent {
        void inject(App app);
    }
    

    那么它的作用就显而易见了——component作为一个容器,封装了它依赖的所有module提供的创建对象的方式。经过dagger的编译以后,我们在目标类调用component实现类的inject方法将它提供的依赖注入进来,之后就可以通过@inject注解从component里面拿到我们所需的对象了。

2. 如何构建依赖关系

我们知道一个android项目必定有application,还可能有activity、fragment、service...这些组件,通常我们会在application(或者单例类)里面保存一些全局变量,以便在其他地方使用;而在activity中也可能需要暴露出共有变量给属于它的fragments使用(比如在有多个fragment的tab页面中),这样的话我们不得不通过get或者setArguments方法传递这些对象,非常地不优雅。下面我们来看看dagger的方式。

2.1 回顾dagger的构建方式

现在我们从dagger的角度来看,我们可以把整个app看作一个根组件,创建一个appComponent;activity看作它的子组件,然后依次创建xxxActivityComponent,将它声明为appComponent的子组件

@Component(modules = {ActivityModule.class, ...})
public interface AppComponent {...}  

@Module(subcomponents = {MainComponent.class, ...})
public abstract class ActivityModule {...}  

@ActivityScope
@Subcomponent
public interface MainComponent {
    void inject(MainActivity activity);

    @Subcomponent.Builder
    interface Builder {
        @BindsInstance
        Builder view(MainContract.View view);

        MainComponent build();
    }
}  

然后在每个activity里面注入它自己的component

App.get(this)
    .getAppComponent()
    .mainComponentBuilder()
    .view(this)
    .build()
    .inject(this);

@BindsInstance: 由于项目用的是MVP模式,需要给presenter提供view实例,这里的view实际上就是activity,它只能通过我们在注入的时候传入(我们无法实例化activity),而@BindsInstance注解的view方法就是把activity作为view实例放到容器里了。

以上这些看起来似乎没什么问题,但是要知道我们一个项目里可能很多个activity,每个都需要这样去注入的话是非常麻烦的,而且这种注入方式看起来并不是一目了然。

下面我们来看看dagger.android的注入方式。

2.2 dagger.android的构建方式

首先build.gradle中加入依赖包

implementation "com.google.dagger:dagger:$version_dagger2"
annotationProcessor "com.google.dagger:dagger-compiler:$version_dagger2"
implementation "com.google.dagger:dagger-android:$version_dagger2"
implementation "com.google.dagger:dagger-android-support:$version_dagger2"
annotationProcessor "com.google.dagger:dagger-android-processor:$version_dagger2"

接下来直接看看最终如何在目标类中的注入依赖

AndroidInjection.inject(四大组件或者fragment)
AndroidSupportInjection.inject(v4包下的fragment)

注意,需要BroadcastReceiver中super.onReceive()之前调用, fragment super.onAttach()之前,其他三大组件super.onCreate()之前调用。

如此简单!不再需要显式地在目标类中指明要注入哪个component,我们甚至能把它放到基类中就可以完成注入。这是怎么做到的呢?我们待会再来细说,再此之前我们还需要做一些别的事情。

  1. AppComponent需要做一些调整

    @Component(modules = {
            AndroidInjectionModule.class,
            AndroidSupportInjectionModule.class,
            ActivityModule.class
    })
    public interface AppComponent {...}
    

    AndroidInjectionModule, AndroidSupportInjectionModule分别是为了保证四大组件和fragment能够被注入到容器中,后者是为了支持v4包下的fragment。

  2. 声明activity对应的子component方式需要改变

     @ActivityScope
     @Subcomponent(modules = {MainModule.class})
     public interface MainComponent extends AndroidInjector<MainActivity> {
    
         @Subcomponent.Builder
         abstract class Builder extends AndroidInjector.Builder<MainActivity> {}
     }  
    
     @Module
     public abstract class MainModule {
         @ActivityScope
         @Binds
         abstract MainContract.View provideView(MainActivity activity);
     }
    

    子component需要继承AndroidInjector<四大组件或者fragment>接口,同时builder也需要继承AndroidInjector.Builder<四大组件或者fragment>。AndroidInjector是干吗用的?我们可以把它理解为注入器,有将commponent注入到目标类能力的东西,但它只是一个接口,具体的实现在子类(就是我们的component的实现类,由dagger编译后生成,代码位于build/source/apt/.../DaggerAppComponent.java)。

    你可能发现我们少了一些东西,我们并没有像之前那样把activity作为MVP中的view传入component,而是直接在module里面直接拿到了activity,那么这个activity是从哪里来的呢?还记得AndroidInjection.inject(xxxActivity)吗,当我们在给activity注入component的时候,同时也将activity本身传入进去了,所以我们可以直接用它。

  3. 需要告诉dagger如何创建一个子component
    首先我们创建一个ActivityModule,用来管理所有activity对应的component

     @Module(subcomponents = MainComponent.class)
     public abstract class ActivityModule {
         @Binds
         @IntoMap
         @ActivityKey(MainActivity.class)
         abstract AndroidInjector.Factory<? extends Activity>  
         bindMainActivity(MainComponent.Builder builder);
     }
    

    这段代码实际上是告诉dagger MainActivity所对应的component是哪一个,简单点说,就是将activity的具体类型作为key,它对应的component的创建方式(AndroidInjector.Factory<? extends Activity>,也就是component的工厂对象)作为值存到一个map里面,然后我们通过AndroidInjection.inject(xxxActivity)注入时,dagger就能通过传入的类型去拿到对应的工厂实例,创建component然后注入进来。当然,如果我们需要注入的是其他组件,那么@ActivityKey就应该换成对应的xxxKey了。

  4. 在application里面注入根component

    public class App extends Application implements HasActivityInjector {
       @Inject
       DispatchingAndroidInjector<Activity> actInjector;
    
       @Override
       public void onCreate() {
           super.onCreate();
           DaggerAppComponent.builder()
                   .appModule(new AppModule(this))
                   .build()
                   .inject(this);
       }
    
       @Override
       public AndroidInjector<Activity> activityInjector() {
           return actInjector;
       }
    }
    

    如果我们要用dagger.android的方式在activity中注入依赖,我们的application就需要实现HasActivityInjector接口,它只有一个抽象方法,需要返回一个AndroidInjector<Activity>的实例,而AndroidInjector本身是一个接口。这时候我们通过@inject注入一个DispatchingAndroidInjector<Activity>进来就好了(如果需要用到其他组件或者fragment,也要实现对应的HasXXXInjector接口),最后别忘了注入根component。

    那么这个DispatchingAndroidInjector有什么作用?它从哪里来到哪里去呢?我们来看一段它的源码。

    @Beta
    public final class DispatchingAndroidInjector<T> implements AndroidInjector<T> {
       ...
       private final Map<Class<? extends T>, Provider<AndroidInjector.Factory<? extends T>>>
           injectorFactories;
    
       @Inject
       DispatchingAndroidInjector(
           Map<Class<? extends T>, Provider<AndroidInjector.Factory<? extends T>>> injectorFactories) {
         this.injectorFactories = injectorFactories;
       }
    }
    

    到这里我们应该能马上明白了,它的构造方法被@inject注解了,所以可以直接被注入。而它内部保存了一个map,在构造器中被注入赋值。其实这个map就是之前我们在上一节讲过的,保存了每个activity(或其他组件)对应的component的工厂实例。这个map又是从哪来的呢?答案就在AndroidInjectionModule里,还记得吗,我们在AppComponent中安装了它,现在来看一看它的代码。

     @Beta
     @Module
     public abstract class AndroidInjectionModule {
       @Multibinds
       abstract Map<Class<? extends Activity>, AndroidInjector.Factory<? extends Activity>>
           activityInjectorFactories();
    
       @Multibinds
       abstract Map<Class<? extends Fragment>, AndroidInjector.Factory<? extends Fragment>>
           fragmentInjectorFactories();
           ......
     }
    

    一目了然,它直接对AppComponent提供了装有各种component工厂实例的map。

    @Multibinds 这个注解需要结合之前创建的ActivityModule来看

    @Binds
    @IntoMap
    @ActivityKey(MainActivity.class)
    abstract AndroidInjector.Factory<? extends Activity>  
    bindMainActivity(MainComponent.Builder builder);
    

    @Binds 把builder转化为它的父类AndroidInjector.Factory<T>,然后@IntoMap 将它绑定到@Multibinds 注解的AndroidInjectionModule中提供Map<T, T>类型依赖的方法,这样就根据<T>分类将每个子component的工厂实例放入一个Map<T, T>中,然后作为依赖提供出去。

3. 更优雅的方式

写了写么多东西,看起来dagger.adroid的方式好像并没有好到哪里去,只是避免了在每个目标类中写一长串的注入代码,但是别急,我们还有终极大招。
  先来看看最终代码

@Module
public abstract class ActivityModule {
    @ActivityScope
    @ContributesAndroidInjector(modules = MainModule.class)
    abstract MainActivity provideMainActivity();
}

ActivityModule直接简化成了这个样子,而且我们不再需要MainComponent(不再需要显式地创建子Component),子component依赖的module现在直接声明在@ContributesAndroidInjector里面就好了。

@ContributesAndroidInjector 做了什么呢?其实一切并没有变,它只是隐式地帮我们创建了一个子component,看看编译后的代码build/source/apt/.../ActivityModule_ProvideMainActivity

@Module(
  subcomponents = ActivityModule_ProvideMainActivity.MainActivitySubcomponent.class
)
@Generated("dagger.android.processor.AndroidProcessor")
public abstract class ActivityModule_ProvideMainActivity {
  private ActivityModule_ProvideMainActivity() {}

  @Binds
  @IntoMap
  @ActivityKey(MainActivity.class)
  abstract AndroidInjector.Factory<? extends Activity> bindAndroidInjectorFactory(
      MainActivitySubcomponent.Builder builder);

  @Subcomponent(modules = MainModule.class)
  @ActivityScope
  public interface MainActivitySubcomponent extends AndroidInjector<MainActivity> {
    @Subcomponent.Builder
    abstract class Builder extends AndroidInjector.Builder<MainActivity> {}
  }
}

看吧,一切并没有变。

4. 从源码理清思路

知其然不知其所以然往往容易犯错,上一部分的结尾我们分析了DispatchingAndroidInjector的一些东西,现在我们来跟着源码从头理一理思路。
  还是从AndroidInjection.inject(xxx)开始,我们点进源码

public static void inject(Activity activity) {
   checkNotNull(activity, "activity");
   Application application = activity.getApplication();
   if (!(application instanceof HasActivityInjector)) {
     throw new RuntimeException(
         String.format(
             "%s does not implement %s",
             application.getClass().getCanonicalName(),
             HasActivityInjector.class.getCanonicalName()));
   }

   AndroidInjector<Activity> activityInjector =
       ((HasActivityInjector) application).activityInjector();
   checkNotNull(activityInjector, "%s.activityInjector() returned null", application.getClass());

   activityInjector.inject(activity);
 }

很简单,判断application是否实现了HasActivityInjector接口,然后调用application的activityInjector函数拿到AndroidInjector实例(正是我们之前在applicaton中@inject进去的DispatchingAndroidInjector),最后调用了DispatchingAndroidInjector的inject方法,我们接着往下面看,回到DispatchingAndroidInjector

@Beta
public final class DispatchingAndroidInjector<T> implements AndroidInjector<T> {
  ......
  @Override
  public void inject(T instance) {
    boolean wasInjected = maybeInject(instance);
    if (!wasInjected) {
      throw new IllegalArgumentException(errorMessageSuggestions(instance));
    }
  }  

  @CanIgnoreReturnValue
  public boolean maybeInject(T instance) {
    Provider<AndroidInjector.Factory<? extends T>> factoryProvider =
        injectorFactories.get(instance.getClass());
    if (factoryProvider == null) {
      return false;
    }

    @SuppressWarnings("unchecked")
    AndroidInjector.Factory<T> factory = (AndroidInjector.Factory<T>) factoryProvider.get();
    try {
      AndroidInjector<T> injector =
          checkNotNull(
              factory.create(instance), "%s.create(I) should not return null.", factory.getClass());

      injector.inject(instance);
      return true;
    } catch (ClassCastException e) {
      throw new InvalidInjectorBindingException(
          String.format(
              "%s does not implement AndroidInjector.Factory<%s>",
              factory.getClass().getCanonicalName(), instance.getClass().getCanonicalName()),
          e);
    }
  }
}

我们直接看最终调用的maybeInject(),它根据我们传入的目标类的class从map中拿到对应的component(我们的子component全部实现了AndroidInjector接口,所以这里的injector就是component)的工厂实例,然后创建了component,最后调用它的inject方法将依赖注入到目标类的实例里面。具体的注入过程可以在dagger为我们生成的xxxComponentImpl里面看到,它是DaggerAppComponent的内部类。

现在来总结一下,首先AndroidInjector接口只有一个inject抽象方法,我们的子component和DispatchingAndroidInjector都是它的子类,当通过AndroidInjection.inject(xxx)注入的时候,实际上是调用application里的DispatchingAndroidInjector的inject方法,而它本身并不做实际的注入,只是起了一个分发的作用,最后找到并调用了当前子component(injecttor)的inject方法进行实际的依赖注入。

5. 结合实例

demo地址:https://github.com/ColorfulHorse/MVPDemo

demo很简单,主要就是一个mainActivity通过viewPager管理了三个fragment,weeklyFragment里面请求了一个七天的天气接口,列表展示了出来,通过数据库做了本地缓存。
看看AppComponent

@Singleton
@Component(modules = {
        AndroidInjectionModule.class,
        AppModule.class,
        OkhttpModule.class,
        RetrofitModule.class,
        ApiServiceModule.class,
        DBModule.class,
        ActivityModule.class})
public interface AppComponent {
    void inject(App app);

    @Component.Builder
    interface Builder {
        @BindsInstance
        Builder application(Application application);
        AppComponent build();
    }
}

依赖了若干module,分别提供okhttpClient实例,retrofit实例,retrofitService实例,数据库实例;这里数据库用的是ObjectBox库,有兴趣的可以了解一下。
其实也不需要创建这么多module,也可以全部放到一个appModule里面就可以,我这里只是做了一个分类,具体情况视项目复杂度而定。

再看看AcrivityModule

@Module
public abstract class ActivityModule {

    @ActivityScope
    @ContributesAndroidInjector(modules = {
            MainModule.class,
            MainFragmentModule.class
    })
    abstract MainActivity provideMainActivity();

    @ContributesAndroidInjector
    @ActivityScope
    abstract ForecastActivity provideForecastActivity();
}

管理了一个列表activity和一个详情activity, MainActivity有一个MainFragmentModule管理其下的三个fragment

@Module
public abstract class MainFragmentModule {

    @FragmentScope
    @ContributesAndroidInjector
    abstract DailyFragment provideDailyFragment();

    @FragmentScope
    @ContributesAndroidInjector(modules = WeeklyModule.class)
    abstract WeeklyFragment provideWeeklyFragment();

    @FragmentScope
    @ContributesAndroidInjector
    abstract MonthlyFragment provideMonthlyFragment();

}

这样看起来整个结构就比较明朗,依赖关系层次明显,appComponent为根组件,其下activityComponent为子组件,再下面还有fragment对应的子组件。当然这一切只是一个简单的示例,也许还有更好的方式等你探索。

本文到这里也就结束了,更多的东西demo里面都写得比较完整,就不再细讲。
如有不足之处欢迎指出。

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

推荐阅读更多精彩内容