美文网首页ITBOX技术Android优秀开源
从零开始的Android新项目4 - Dagger2篇

从零开始的Android新项目4 - Dagger2篇

作者: MarkZhai | 来源:发表于2016-04-01 11:24 被阅读2874次

    Dagger - 匕首,顾名思义,比ButterKnife这把黄油刀锋利得多。Square为什么这么有自信地给它取了这个名字,Google又为什么会拿去做了Dagger2呢(不都有Guice和基于其做的RoboGuice了么)?希望本文能讲清楚为什么要用Dagger2,又如何用好Dagger2。

    本文会从Dagger2的起源开始,途径其初衷、使用场景、依赖图,最后介绍一下我在项目中的具体应用和心得体会。

    Origin

    Dagger2,起源于Square的Dagger,是一个完全在编译期间进行的依赖注入框架,完全去除了反射。

    关于Dagger2的最初想法,来自于2013年12月的Proposal: Dagger 2.0,Jake大神在issue里面也有回复哦,而idea的来源者Gregory Kick的GitHub个人主页也没多少follower,自己也没几个项目,主要都在贡献其他的repository,可见海外重复造轮子的风气比我们这儿好多了。

    扯远了,Dagger2的诞生就是源于开发者们对Dagger1半静态化半运行时的不满(尤其是在服务端的大型应用上),想要改造成完整的静态依赖图生成,完全的代码生成式依赖注入解决方案。在权衡了什么对Android更适合,以及对大型应用来说什么更有意义(往往有可怕数量的注入)两者后,Dagger2诞生了。

    初衷

    Dagger2的初衷就是装逼,啊,不对,是通过依赖注入让你少些很多公式化代码,更容易测试,降低耦合,创建可复用可互换的模块。你可以在Debug包,测试运行包以及release包优雅注入三种不同的实现。

    依赖注入

    说到依赖注入,或许很多以前做过JavaEE的朋友会想到Spring(SSH在我本科期间折磨得我欲生欲死,最后Spring MVC拯救了我)。

    我们看个简单的比较图,左边是没有依赖注入的实现方式,右边是手动的依赖注入:


    Without DI and with Maunl DI

    我们想要一个咖啡机来做一杯咖啡,没有依赖注入的话,我们就需要在咖啡机里自己去new泵(pump)和加热器(heater),而手动依赖注入的实现则将依赖作为参数,然后传入,而不是自己去显示创建。在没有依赖注入的时候,我们丧失了灵活性,因为一切依赖是在内部创建的,所以我们根本没有办法去替换依赖实例,比如想把电加热器换成火炉或者核加热器,看一看下图,是不是更清晰了:


    Without DI and with Maunl DI

    为什么我们需要DI库

    但问题在于,在大型应用中,把这些依赖全都分离,然后自己去创建的话,会是一个很大的工作量——毫无营养的公式化代码,一堆Factory类。不仅仅是工作量的问题,这些依赖可能还有顺序的问题,A依赖B,B依赖C,B依赖D,如此一来C、D就必须在A、B的后面,手动去做这些工作简直是一个噩梦 =。=(哈哈,是不是想到了appliation初始化那些依赖)。Google的工程师碰到的问题就是在Android上有3000行这样的代码,而在服务器上的大型程序则是100000行。

    你会想自己维护这样的代码吗?

    Why Dagger2

    先来看看如果用Spring实现上面提到的咖啡机依赖,我们需要做什么:


    DI with Spring

    不错,就是xml,当然,我们也不需要去关心顺序了,Spring会帮我们解决前后顺序的依赖问题。

    但仔细想想,你会想去自己写这样的xml代码吗?layout.xml已经写得我很烦了。而且Spring是在运行时验证配置和依赖图的,你不会想在外网运行的app里让用户发现你的依赖注入出了问题的(比如bean名字打错了)。再加上xml和Java代码分离,很难追踪应用流。

    Guice虽然较Spring进了一步,干掉了xml,通过Java声明依赖注入比起Spring好找多了,但其跟踪和报错(运行时的图验证)实在令人抓狂,而且在不同环境注入不同实例的配置也挺恶心的(if else各种判断),感兴趣的可以去看看,项目就在GitHub上,Android版本的叫RoboGuice。

    而Dagger2和Dagger1的差别在上节已经提到了,更专注于开发者的体验,从半静态变为完全静态,从Map式的API变成申明式API(@Module),生成的代码更优雅,更高的性能(跟手写一样),更简单的debug跟踪,所有的报错也都是在编译时发生的。

    Dagger2使用了JSR 330的依赖注入API,其实就是Provider了:

    public interface Provider<T> {
      T get();
    }
    
    // Usage:
    Provider<T> coffeeMakerProvider = ...;
    CoffeeMaker coffeeMaker = coffeeMakerProvider.get();
    

    Dagger2基于Component注解:

    @Component(modules = DripCoffeModule.class)
    interface CoffeeMakerComponet {
      CoffeeMaker getCoffeeMaker();
    }
    
    // 会生成这样的代码,Dagger_CoffeeMakerComponent里面就是一堆Provider,
    // 或者是单例,或者是通过DripCoffeeModule申明new的方式,开发者不必关心依赖顺序
    CoffeeMakerComponent component = Dagger_CoffeeMakerComponent.create();
    CoffeeMaker coffeeMaker = component.getCoffeeMaker();
    

    除了上面提到的各种好处,不得不提的是也有对应问题:丧失了动态性,在之后的实践中我会举个例子描述一下,但相对于那些好处来说,我觉得是可接受的。Everything has a Price to Pay。啊,对了,还有另一点,没法自动升级,从Dagger1到Dagger2,当然如果你的app是没有历史负担的(本系列的前提),那这不算问题。

    如果对性能感兴趣的话,可以去看看Comparing the Performance of Dependency Injection Libraries,RoboGuice:Dagger1:Dagger2差不多是50:2:1的一个性能差距。

    如果你用了Dagger2,而你的服务端还在用Spring,你可以自豪地说,我们比你们领先5年。而Google的服务端确实已经用了Dagger2。

    使用场景

    上面也曾经提到了,因为手动去维护那些依赖关系、范围很麻烦,就连单例我都懒得写,何况是各种Factory类,老在那synchroized烦不烦。而如果不去写那些Factory,直接new,则会导致后期维护困难,比如增加了一个参数,为了保证兼容性,就只能留着原来的构造函数(习惯好一点的标一下deprecated),再新增一个构造函数。

    Dagger2解决了这些问题,帮助我们管理实例,并进行解耦。new只需要写在一个地方,getInstance也再也不用写了。而需要使用实例的地方,只需要简简单单地来一个@inject,而不需要关心是如何注入的。Dagger2会在编译时通过apt生成代码进行注入。

    想想你所有可能在多个地方使用的类实例依赖,比如lbs服务,比如你的cache,比如用户设置,比起getInstance,比起new,比起自己用注释去注明必须维持这种先后关系(说到此处,想到上个东家的android app初始化时候,必须保持正确顺序不然立马crash,singleton还必须只能init一次的糟糕代码),为什么不用dagger来做管理?Without any performance overhead。

    Dagger2基于编译时的静态依赖图构建还能避免运行时再出现一些坑,比如循环依赖,编译的时候就会报错,而不会在运行时死循环。

    生动点来说的话。有一场派对:

    Android开发A说,有妹子我才来。
    美女前端B说,有帅哥设计师,我才来。
    iOS开发C说,有Android开发,我才来。
    帅哥设计师说,只有礼拜天我才有空。

    class AndroidDeveloper extends PartyMember {
        public AndroidDeveloper(PartyMember female) throws NotMeizhiSayBB;
    }
    
    public class FrontEndDeveloper extends PartyMember {
        public FrontEndDeveloper(Designer designer) throws NotHandsomeBoySayBB;
    }
    
    class IOSDeveloper extends PartyMember {
        public IOSDeveloper(AndroidDeveloper dev);
    }
    
    class Designer extends PartyMember {
        public Designer(Date date) throw CannotComeException;
    }
    
    class PartyMember {
        private int mSex = 0; // 1 for male, 2 for female.
        public void setSex(int sex);
    }
    
    // 手动DI,要自己想怎么设计顺序,还不能轻易改动
    Designer designer = new Designer("礼拜天");
    FrontEndDeveloper dev1 = new FrontEndDeveloper(designer);
    dev1.setSex(2);
    AndroidDeveloper dev2 = new AndroidDeveloper(dev1);
    IOSDeveloper dev3 = new IOSDeveloper(dev2);
    
    // With Dagger2
    @Inject
    Designer designer;
    @Inject
    FrontEndDeveloper dev1;
    @Inject
    AndroidDeveloper dev2;
    @Inject
    IOSDeveloper dev3;
    
    // 不使用DI太可怕了...自己想象一下会是什么样吧
    ...我懒
    

    Scope

    Dagger2的Scope,除了Singleton(root),其他都是自定义的,无论你给它命名PerActivity、PerFragment,其实都只是一个命名而已,真正起作用的是inject的位置,以及dependency。

    Scope起的更多是一个限制作用,比如不同层级的Component需要有不同的scope,注入PerActivity scope的component后activity就不能通过@Inject去获得SingleTon的实例,需要从application去暴露接口获得(getAppliationComponent获得component实例然后访问,比如全局的navigator)。

    当然,另一方面则是可读性和方便理解,通过scope的不同很容易能辨明2个实例的作用域的区别。

    依赖图例子

    Simple Graph

    如上是一个我现在使用的Dagger2的依赖图的简化版子集。

    ApplicationComponent作为root,拆分出了3个module

    • ApplicationModule(application context,lbs服务,全局设置等)
    • ApiModule(Retrofit那堆Api在这里)
    • RepositoryModule(各种repository)。
      这里为了妥协内聚和简洁所以保持了这三个module。你不会想看到自己的di package下有一大堆module类,或者某个module里面掺杂着上百个实例注入的。

    UserComponent用在用户主页、登录注册,以及好友列表页。所以你能看到UserModule(用户系统以及那些UseCase)以及需要的赞Module、相册Module。

    TagComponent是标签系统,有自己的标签Module以及赞Module(module重用),用在了标签搜索、热门标签等页面。

    是不是很好理解?位于上层的component是看不到下层的,而下层则可以使用上层的,但不能引用同一层相邻component内的实例。

    如果你的应用是强登录态的,则更可以只把UserComponent放在第二层,Module构造函数传入uid(PerUser scope,没有uid则为游客态,供deeplink之类使用),而所有需要登录态的则都放在第三层。

    一个简单的应用就是这样了,而Component继承,SubComponent(共享的放在上层父类),不同component的module复用(一样可以生成实例绑定,只是没法共享component中暴露的接口罢了)这些则是不同场景下的策略,如果有必要我会再开一篇讲讲这些深入的使用。

    具体应用和心得体会

    • No Proguard rules need。因为0反射,所以完全不需要去配置proguard规则。

    • 因为需要静态地去inject,如果一些参数需要运行时通过用户行为去获得,就只能使用set去设置注入实例的参数(因为我们的injection通常在最早,比如onCreate就需要执行)。这就是上文提到过的,因为完全静态而丧失了一定的动态性。

    • Singleton是线程安全的,请放心,如果实在怀疑,可以去检查生成的源码,笔者已经检查过了...

    • 粒度的问题,如果基于页面去划分的话,老实说笔者觉得实在太细太麻烦,建议稍微粗一点,按照大功能去分,完全可以通过拆分module或者SubComponent的形式去解决复用的问题,而不用拆分出一大堆component,module只要足够内聚就可以,而不需要拆分到某个页面使用的那些。

    • fragment的问题,因为其诡异的生命周期,所以建议在实在需要fragment的时候,让activity去创建component,fragment通过接口(比如HasComponent)去获得component(一个activity只能inject一个component哦)。

    • 举一个我遇到的例子来说说方便的地方,有一个UseCase叫做SearchTag,原先只需要TagRepository,ThreadExecutor,PostThreadExecutor三个参数。现在需求改变了,需要在发起请求前先进行定位,然后把位置信息也作为请求的参数。我们只需要简单地在构造函数增加一个LbsRepository,然后在buildUseCaseObservable通过RxJava组合一下,这样既避免了底层repository的耦合,又对上层屏蔽了复杂性。

    • 再讲讲之前提到的依赖吧,我们有很多同级的实例,以Singleton为例,比如有一个要提供给第三方sdk的Provider依赖了某个Repository,直接在构造函数里加上那个Repository,然后加上@Inject,完全不需要关心前后顺序了,省不省心?还可以随时在单元测试的包注入一个不需要物理环境的模拟repository。想想以前你怎么做,或者在调用这个的初始化前init依赖的实例,或者在初始化里去使用依赖类的getInstance(),是不是太土鳖?

    • 强烈推荐你在自己的项目里使用上,初期可能怀着装逼的心情觉得有点麻烦,熟练后你会发现简直太方便了,根本离不开(其实是我的亲身经历 哈哈)。

    总结

    本篇讲了讲Dagger2,主要还是在安利为什么要用Dagger2,以及一些正确的使用姿势,因为时间原因来不及写个demo来说说具体实现,欢迎大家提出意见和建议。
    有空的话我最近会在GitHub上写一下demo,你如果有兴趣可以follow一下等等更新: markzhai(希望在4月能完成,哈哈...)。

    下集预告

    怎么用Retrofit、Realm和RxJava搭建data层。

    参考文献

    原文链接:http://blog.zhaiyifan.cn/2016/03/27/android-new-project-from-0-p4/

    相关文章

      网友评论

      • franklin_:楼主 有没有写demo呀,,光看文章感觉不过瘾
      • T嗯T:你的观点:“Guice较Spring进了一步”、“Dagger2比Spring领先5年”是错误的,因为2009年推出的Spring 3.0已经提供对 JSR-330的支持。
      • MarkZhai:是需要的呀,如果你的component跟appcomponent不在同一级,就需要提供更小的scope,不能用singleton。scope起的是一个检查的作用,只有同一层级的才能用同一个scope
        MarkZhai:@苦逼键盘男kkmike999 是哒
        键盘男:@MarkZhai 那就是说某个scope可以给多个component用,只要他们层级一样?
      • 键盘男:Mark大哥,关于Scope我还是不太懂。AppComponent使用AppModule,提供@Singleton 实例(例如Application);UserComponent dependencies = AppComponent.class, modules = UserModule.class。编译时提示要UserComponent要使用自定义@Scope,后来加了@UserScope才能编译通过.... (https://github.com/codepath/android_guides/wiki/Dependency-Injection-with-Dagger-2
        MarkZhai:@苦逼键盘男kkmike999 是需要的呀,如果你的component跟appcomponent不在同一级,就需要提供更小的scope,不能用singleton。scope起的是一个检查的作用,只有同一层级的才能用同一个scope。

        如果你都想用singleton,那只能全都定义在AppComponent的module里面
      • MarkZhai:恩恩,对的
        TagComponent里面就有 inject( TagActivityA ) & inject( TagActivityB )
      • 键盘男:Mark大神写得很好!就是图裂了 :joy:

        小弟有个问题,因为每个Activity都会调用component.inject(...),那component是根据功能分吗?你说UserComponent、TagComponent,如果使用标签系统有TagActivityA & TagActivityB,那TagComponent里面就有 inject( TagActivityA ) & inject( TagActivityB ) 吗?
        MarkZhai:@苦逼键盘男kkmike999 ApplicationComponent在Application.onCreate,其他都在activity的onCreate
        键盘男:@MarkZhai 那UserComponent、TagComponent是在Application.onCreate()时build(),还是在Activity.onCreate()时build()?
        MarkZhai:@苦逼键盘男kkmike999 恩恩,对的
        TagComponent里面就有 inject( TagActivityA ) & inject( TagActivityB )

      本文标题:从零开始的Android新项目4 - Dagger2篇

      本文链接:https://www.haomeiwen.com/subject/zekylttx.html