從零開始的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的誕生就是源于開發(fā)者們對Dagger1半靜態(tài)化半運行時的不滿(尤其是在服務端的大型應用上),想要改造成完整的靜態(tài)依賴圖生成奥邮,完全的代碼生成式依賴注入解決方案万牺。在權衡了什么對Android更適合,以及對大型應用來說什么更有意義(往往有可怕數量的注入)兩者后洽腺,Dagger2誕生了脚粟。

初衷

Dagger2的初衷就是裝逼,啊蘸朋,不對核无,是通過依賴注入讓你少些很多公式化代碼,更容易測試藕坯,降低耦合团南,創(chuàng)建可復用可互換的模塊噪沙。你可以在Debug包,測試運行包以及release包優(yōu)雅注入三種不同的實現已慢。

依賴注入

說到依賴注入曲聂,或許很多以前做過JavaEE的朋友會想到Spring(SSH在我本科期間折磨得我欲生欲死霹购,最后Spring MVC拯救了我)佑惠。

我們看個簡單的比較圖,左邊是沒有依賴注入的實現方式齐疙,右邊是手動的依賴注入:


Without DI and with Maunl DI

我們想要一個咖啡機來做一杯咖啡膜楷,沒有依賴注入的話,我們就需要在咖啡機里自己去new泵(pump)和加熱器(heater)贞奋,而手動依賴注入的實現則將依賴作為參數赌厅,然后傳入,而不是自己去顯示創(chuàng)建轿塔。在沒有依賴注入的時候特愿,我們喪失了靈活性,因為一切依賴是在內部創(chuàng)建的勾缭,所以我們根本沒有辦法去替換依賴實例揍障,比如想把電加熱器換成火爐或者核加熱器,看一看下圖俩由,是不是更清晰了:


Without DI and with Maunl DI

為什么我們需要DI庫

但問題在于毒嫡,在大型應用中,把這些依賴全都分離幻梯,然后自己去創(chuàng)建的話兜畸,會是一個很大的工作量——毫無營養(yǎng)的公式化代碼,一堆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里讓用戶發(fā)現你的依賴注入出了問題的(比如bean名字打錯了)享完。再加上xml和Java代碼分離灼芭,很難追蹤應用流。

Guice雖然較Spring進了一步般又,干掉了xml彼绷,通過Java聲明依賴注入比起Spring好找多了,但其跟蹤和報錯(運行時的圖驗證)實在令人抓狂茴迁,而且在不同環(huán)境注入不同實例的配置也挺惡心的(if else各種判斷)寄悯,感興趣的可以去看看,項目就在GitHub上笋熬,Android版本的叫RoboGuice热某。

而Dagger2和Dagger1的差別在上節(jié)已經提到了,更專注于開發(fā)者的體驗胳螟,從半靜態(tài)變?yōu)橥耆o態(tài)昔馋,從Map式的API變成申明式API(@Module),生成的代碼更優(yōu)雅糖耸,更高的性能(跟手寫一樣)秘遏,更簡單的debug跟蹤,所有的報錯也都是在編譯時發(fā)生的嘉竟。

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的方式,開發(fā)者不必關心依賴順序
CoffeeMakerComponent component = Dagger_CoffeeMakerComponent.create();
CoffeeMaker coffeeMaker = component.getCoffeeMaker();

除了上面提到的各種好處边苹,不得不提的是也有對應問題:喪失了動態(tài)性陵且,在之后的實踐中我會舉個例子描述一下,但相對于那些好處來說个束,我覺得是可接受的慕购。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基于編譯時的靜態(tài)依賴圖構建還能避免運行時再出現一些坑奏属,比如循環(huán)依賴跨跨,編譯的時候就會報錯,而不會在運行時死循環(huán)囱皿。

生動點來說的話勇婴。有一場派對:

Android開發(fā)A說,有妹子我才來嘱腥。
美女前端B說耕渴,有帥哥設計師,我才來齿兔。
iOS開發(fā)C說橱脸,有Android開發(fā),我才來分苇。
帥哥設計師說添诉,只有禮拜天我才有空。

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個實例的作用域的區(qū)別。

依賴圖例子

Simple Graph

如上是一個我現在使用的Dagger2的依賴圖的簡化版子集宰睡。

ApplicationComponent作為root蒲凶,拆分出了3個module

  • ApplicationModule(application context,lbs服務拆内,全局設置等)
  • ApiModule(Retrofit那堆Api在這里)
  • RepositoryModule(各種repository)旋圆。
    這里為了妥協(xié)內聚和簡潔所以保持了這三個module。你不會想看到自己的di package下有一大堆module類麸恍,或者某個module里面摻雜著上百個實例注入的灵巧。

UserComponent用在用戶主頁、登錄注冊或南,以及好友列表頁孩等。所以你能看到UserModule(用戶系統(tǒng)以及那些UseCase)以及需要的贊Module艾君、相冊Module采够。

TagComponent是標簽系統(tǒng),有自己的標簽Module以及贊Module(module重用)冰垄,用在了標簽搜索蹬癌、熱門標簽等頁面。

是不是很好理解虹茶?位于上層的component是看不到下層的逝薪,而下層則可以使用上層的,但不能引用同一層相鄰component內的實例蝴罪。

如果你的應用是強登錄態(tài)的董济,則更可以只把UserComponent放在第二層,Module構造函數傳入uid(PerUser scope要门,沒有uid則為游客態(tài)虏肾,供deeplink之類使用),而所有需要登錄態(tài)的則都放在第三層欢搜。

一個簡單的應用就是這樣了封豪,而Component繼承,SubComponent(共享的放在上層父類)炒瘟,不同component的module復用(一樣可以生成實例綁定吹埠,只是沒法共享component中暴露的接口罷了)這些則是不同場景下的策略,如果有必要我會再開一篇講講這些深入的使用。

具體應用和心得體會

  • No Proguard rules need缘琅。因為0反射粘都,所以完全不需要去配置proguard規(guī)則。

  • 因為需要靜態(tài)地去inject刷袍,如果一些參數需要運行時通過用戶行為去獲得驯杜,就只能使用set去設置注入實例的參數(因為我們的injection通常在最早,比如onCreate就需要執(zhí)行)做个。這就是上文提到過的鸽心,因為完全靜態(tài)而喪失了一定的動態(tài)性。

  • Singleton是線程安全的居暖,請放心顽频,如果實在懷疑,可以去檢查生成的源碼太闺,筆者已經檢查過了...

  • 粒度的問題糯景,如果基于頁面去劃分的話,老實說筆者覺得實在太細太麻煩省骂,建議稍微粗一點蟀淮,按照大功能去分,完全可以通過拆分module或者SubComponent的形式去解決復用的問題钞澳,而不用拆分出一大堆component怠惶,module只要足夠內聚就可以,而不需要拆分到某個頁面使用的那些轧粟。

  • fragment的問題策治,因為其詭異的生命周期,所以建議在實在需要fragment的時候兰吟,讓activity去創(chuàng)建component通惫,fragment通過接口(比如HasComponent)去獲得component(一個activity只能inject一個component哦)。

  • 舉一個我遇到的例子來說說方便的地方混蔼,有一個UseCase叫做SearchTag履腋,原先只需要TagRepository,ThreadExecutor惭嚣,PostThreadExecutor三個參數∽窈現在需求改變了,需要在發(fā)起請求前先進行定位料按,然后把位置信息也作為請求的參數奄侠。我們只需要簡單地在構造函數增加一個LbsRepository,然后在buildUseCaseObservable通過RxJava組合一下载矿,這樣既避免了底層repository的耦合垄潮,又對上層屏蔽了復雜性烹卒。

  • 再講講之前提到的依賴吧,我們有很多同級的實例弯洗,以Singleton為例旅急,比如有一個要提供給第三方sdk的Provider依賴了某個Repository,直接在構造函數里加上那個Repository牡整,然后加上@Inject藐吮,完全不需要關心前后順序了,省不省心逃贝?還可以隨時在單元測試的包注入一個不需要物理環(huán)境的模擬repository谣辞。想想以前你怎么做,或者在調用這個的初始化前init依賴的實例沐扳,或者在初始化里去使用依賴類的getInstance()泥从,是不是太土鱉?

  • 強烈推薦你在自己的項目里使用上沪摄,初期可能懷著裝逼的心情覺得有點麻煩躯嫉,熟練后你會發(fā)現簡直太方便了,根本離不開(其實是我的親身經歷 哈哈)杨拐。

總結

本篇講了講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/

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末塌碌,一起剝皮案震驚了整個濱河市渊胸,隨后出現的幾起案子,更是在濱河造成了極大的恐慌台妆,老刑警劉巖翎猛,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異接剩,居然都是意外死亡切厘,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門懊缺,熙熙樓的掌柜王于貴愁眉苦臉地迎上來疫稿,“玉大人,你說我怎么就攤上這事∫抛” “怎么了舀凛?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長途蒋。 經常有香客問我猛遍,道長,這世上最難降的妖魔是什么号坡? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任懊烤,我火速辦了婚禮,結果婚禮上宽堆,老公的妹妹穿的比我還像新娘奸晴。我一直安慰自己,他們只是感情好日麸,可當我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布寄啼。 她就那樣靜靜地躺著,像睡著了一般代箭。 火紅的嫁衣襯著肌膚如雪墩划。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天嗡综,我揣著相機與錄音乙帮,去河邊找鬼。 笑死极景,一個胖子當著我的面吹牛察净,可吹牛的內容都是我干的。 我是一名探鬼主播盼樟,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼氢卡,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了晨缴?” 一聲冷哼從身側響起译秦,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎击碗,沒想到半個月后筑悴,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡稍途,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年阁吝,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片械拍。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡突勇,死狀恐怖装盯,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情与境,我是刑警寧澤验夯,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站摔刁,受9級特大地震影響挥转,放射性物質發(fā)生泄漏。R本人自食惡果不足惜共屈,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一绑谣、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧拗引,春花似錦借宵、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至哼凯,卻和暖如春欲间,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背断部。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工猎贴, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人蝴光。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓她渴,卻偏偏與公主長得像,于是被迫代替她去往敵國和親蔑祟。 傳聞我的和親對象是個殘疾皇子趁耗,可洞房花燭夜當晚...
    茶點故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內容