從零開始的Android新專案4
轉載請註明出處
Dagger - 匕首,顧名思義,比ButterKnife這把黃油刀鋒利得多。Square為什麼這麼有自信地給它取了這個名字,Google又為什麼會拿去做了Dagger2呢(不都有Guice和基於其做的RoboGuice了麼)?希望本文能講清楚為什麼要用Dagger2,又如何用好Dagger2。
本文會從Dagger2的起源開始,途徑其初衷、使用場景、依賴圖,最後介紹一下我在專案中的具體應用和心得體會。
Origin
Dagger2,起源於Square的Dagger,是一個完全在編譯期間進行的依賴注入框架,完全去除了反射。
關於Dagger2的最初想法,來自於2013年12月的
扯遠了,Dagger2的誕生就是源於開發者們對Dagger1半靜態化半執行時的不滿(尤其是在服務端的大型應用上),想要改造成完整的靜態依賴圖生成,完全的程式碼生成式依賴注入解決方案。在權衡了什麼對Android更適合,以及對大型應用來說什麼更有意義(往往有可怕數量的注入)兩者後,Dagger2誕生了。
初衷
Dagger2的初衷就是裝逼,啊,不對,是通過依賴注入讓你少些很多公式化程式碼,更容易測試,降低耦合,建立可複用可互換的模組。你可以在Debug包,測試執行包以及release包優雅注入三種不同的實現。
依賴注入
說到依賴注入,或許很多以前做過JavaEE的朋友會想到Spring(SSH在我本科期間折磨得我欲生欲死,最後Spring MVC拯救了我)。
我們看個簡單的比較圖,左邊是沒有依賴注入的實現方式,右邊是手動的依賴注入:
我們想要一個咖啡機來做一杯咖啡,沒有依賴注入的話,我們就需要在咖啡機裡自己去new泵(pump)和加熱器(heater),而手動依賴注入的實現則將依賴作為引數,然後傳入,而不是自己去顯示建立。在沒有依賴注入的時候,我們喪失了靈活性,因為一切依賴是在內部建立的,所以我們根本沒有辦法去替換依賴例項,比如想把電加熱器換成火爐或者核加熱器,看一看下圖,是不是更清晰了:
為什麼我們需要DI庫
但問題在於,在大型應用中,把這些依賴全都分離,然後自己去建立的話,會是一個很大的工作量——毫無營養的公式化程式碼,一堆Factory類。不僅僅是工作量的問題,這些依賴可能還有順序的問題,A依賴B,B依賴C,B依賴D,如此一來C、D就必須在A、B的後面,手動去做這些工作簡直是一個噩夢 =。=(哈哈,是不是想到了appliation初始化那些依賴)。Google的工程師碰到的問題就是在Android上有3000行這樣的程式碼,而在伺服器上的大型程式則是100000行。
你會想自己維護這樣的程式碼嗎?
Why Dagger2
先來看看如果用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是沒有歷史負擔的(本系列的前提),那這不算問題。
如果你用了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個例項的作用域的區別。
依賴圖例子
如上是一個我現在使用的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層。
參考文獻
擴充套件閱讀
歡迎關注我們的公眾號:魔都三帥
,歡迎大家來投稿~