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

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/

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

推荐阅读更多精彩内容