Android單元測試(6):使用dagger2來做依賴注入
注:
- 1. 程式碼中的 //<= 表示新加的、修改的等需要重點關注的程式碼
- 2. Class#method表示一個類的instance method,比如 LoginPresenter#login 表示 LoginPresenter的login(非靜態)方法。
問題
在前一篇文章中,我們講述了依賴注入的概念,以及依賴注入對單元測試極其關鍵的重要性和必要性。在那篇文章的結尾,我們遇到了一個問題,那就是如果不使用DI框架,而全部採用手工來做DI的話,那麼所有的Dependency都需要在最上層的client來生成,這可不是件好事情。繼續用我們前面的例子來具體說明一下。
假設有一個登入介面,LoginActivity
LoginPresenter
,LoginPresenter
用到了UserManager
和PasswordValidator
,為了讓問題變得更明顯一點,我們假設UserManager
用到SharedPreference
(用來儲存一些使用者的基本設定等)和UserApiService
,而UserApiService
又需要由Retrofit
建立,而Retrofit
又用到OkHttpClient
(比如說你要自己控制timeout、cache等東西)。
應用DI模式,UserManager的設計如下:
1234567891011 | publicclassUserManager{privatefinalSharedPreferences mPref;privatefinalUserApiService mRestAdapter;publicUserManager(SharedPreferences preferences,UserApiService userApiService){this.mPref=preferences;this.mRestAdapter=userApiService;}/**Other code*/ |
LoginPresenter的設計如下:
1234567891011 | publicclassLoginPresenter{privatefinalUserManager mUserManager;privatefinalPasswordValidator mPasswordValidator;publicLoginPresenter(UserManager userManager,PasswordValidator passwordValidator){this.mUserManager=userManager;this.mPasswordValidator=passwordValidator;}/**Other code*/} |
在這種情況下,最終的client LoginActivity裡面要new一個presenter,需要做的事情如下:
1234567891011121314151617181920212223 | publicclassLoginActivityextendsAppCompatActivity{privateLoginPresenter mLoginPresenter;@OverrideprotectedvoidonCreate(Bundle savedInstanceState){super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);OkHttpClient okhttpClient=newOkHttpClient.Builder().connectTimeout(30,TimeUnit.SECONDS).build();Retrofit retrofit=newRetrofit.Builder().client(okhttpClient).baseUrl("https://api.github.com").build();UserApiService userApiService=retrofit.create(UserApiService.class);SharedPreferences preferences=PreferenceManager.getDefaultSharedPreferences(this);UserManager userManager=newUserManager(preferences,userApiService);PasswordValidator passwordValidator=newPasswordValidator();mLoginPresenter=newLoginPresenter(userManager,passwordValidator);}} |
這個也太誇張了,LoginActivity
所需要的,不過是一個LoginPresenter
而已,然而它卻需要知道LoginPresenter
的Dependency是什麼,LoginPresenter
的Dependency的Dependency又是什麼,然後new一堆東西出來。而且可以預見的是,這個app的其他地方也需要這裡的OkHttpClient
、Retrofit
、SharedPreference
、UserManager
等等dependency,因此也需要new這些東西出來,造成大量的程式碼重複,和不必要的object instance生成。然而如前所述,我們又必須用到DI模式,這個怎麼辦呢?
想想,如果能達到這樣的效果,那該有多好:我們只需要在一個類似於dependency工廠的地方統一生產這些dependency,以及這些dependency的dependency。所有需要用到這些Dependency的client都從這個工廠裡面去獲取。而且更妙的是,一個client(比如說LoginActivity
)只需要知道它直接用到的Dependency(LoginPresenter
),而不需要知道它的Dependency(LoginPresenter
)又用到哪些Dependency(UserManager
和PasswordValidator
)。系統自動識別出這個依賴關係,從工廠裡面把需要的Dependency找到,然後把這個client所需要的Dependency創建出來。
有這樣一個東西,幫我們實現這個效果嗎?相信聰明的你已經猜到了,回答是肯定的,它就是我們今天要介紹的dagger2。
解藥:Dagger2
在dagger2裡面,負責生產這些Dependency的統一工廠叫做 Module ,所有的client最終是要從module裡面獲取Dependency的,然而他們不是直接向module要的,而是有一個專門的“工廠管理員”,負責接收client的要求,然後到Module裡面去找到相應的Dependency,提供給client們。這個“工廠管理員”叫做 Component。基本上,這是dagger2裡面最重要的兩個概念。
下面,我們來看看這兩個概念,對應到程式碼裡面,是怎麼樣的。
生產Dependency的工廠:Module
首先是Module,一個Module對應到程式碼裡面就是一個類,只不過這個類需要用dagger2裡面的一個annotation @Module
來標註一下,來表示這是一個Module,而不是一個普通的類。我們說Module是生產Dependency的地方,對應到程式碼裡面就是Module裡面有很多方法,這些方法做的事情就是建立Dependency。用上面的例子中的Dependency來說明:
123456789101112131415161718 | @ModulepublicclassAppModule{publicOkHttpClient provideOkHttpClient(){OkHttpClient okhttpClient=newOkHttpClient.Builder().connectTimeout(30,TimeUnit.SECONDS).build();returnokhttpClient;}publicRetrofit provideRetrofit(OkHttpClient okhttpClient){Retrofit retrofit=newRetrofit.Builder().client(okhttpClient).baseUrl("https://api.github.com").build();returnretrofit;}} |
在上面的Module(AppModule
)中,有兩個方法provideOkHttpClient()
和provideRetrofit(OkHttpClient okhttpClient)
,分別建立了兩個Dependency,OkHttpClient
和Retrofit
。但是呢,我們也說了,一個Module就是一個類,這個類有一些生產Dependency的方法,但它也可以有一些正常的,不是用來生產Dependency的方法。那怎麼樣讓管理員知道,一個Module裡面哪些方法是用來生產Dependency的,哪些不是呢?為了方便做這個區分,dagger2規定,所有生產Dependency的方法必須用 @Provides
這個annotation標註一下。所以,上面的 AppModule
正確的寫法應該是:
12345678910111213141516171819 | @ModulepublicclassAppModule{@ProvidespublicOkHttpClient provideOkHttpClient(){OkHttpClient okhttpClient=newOkHttpClient.Builder().connectTimeout(30,TimeUnit.SECONDS).build();returnokhttpClient;}@ProvidespublicRetrofit provideRetrofit(OkHttpClient okhttpClient){Retrofit retrofit=newRetrofit.Builder().client(okhttpClient).baseUrl("https://api.github.com").build();returnretrofit;}} |
這種用來生產Dependency的、用 @Provides
修飾過的方法叫Provider方法。這裡要注意第二個Provider方法 provideRetrofit(OkHttpClient okhttpClient)
,這個方法有一個引數,是OkHttpClient
。這是因為建立一個Retrofit
物件需要一個OkHttpClient
的物件,這裡通過引數傳遞進來。這樣做的好處是,當Client向管理員(Component)索要一個Retrofit
的時候,Component會自動找到Module裡面找到生產Retrofit的這個 provideRetrofit(OkHttpClient okhttpClient)
方法,找到以後試圖呼叫這個方法建立一個Retrofit
物件,返回給Client。但是呼叫這個方法需要一個OkHttpClient
,於是Component又會去找其他的provider方法,看看有沒有哪個會生產OkHttpClient
。於是就找到了上面的第一個provider方法: provideOkHttpClient()
。
找到以後,呼叫這個方法,建立一個OkHttpClient
物件,再呼叫 provideRetrofit(OkHttpClient okhttpClient)
方法,把剛剛建立的OkHttpClient
物件傳進去,創建出一個Retrofit
物件,返回給Client。當然,如果最後找到的 provideOkHttpClient()
方法也需要其他引數,那麼管理員還會繼續遞迴的找下去,直到所有的Dependency都被滿足了,再一個一個建立Dependency,然後把最終Client需要的Dependency呈遞給Client。
很好,現在我們把文章開頭的例子中的所有Dependency都用這種方式,在 AppModule
裡面宣告一個provider方法:
1234567891011121314151617181920212223242526272829303132333435363738394041424344 | @ModulepublicclassAppModule{@ProvidespublicOkHttpClient provideOkHttpClient(){OkHttpClient okhttpClient=newOkHttpClient.Builder().connectTimeout(30,TimeUnit.SECONDS).build();returnokhttpClient;}@ProvidespublicRetrofit provideRetrofit(OkHttpClient okhttpClient){Retrofit retrofit=newRetrofit.Builder().client(okhttpClient).baseUrl("https://api.github.com").build();returnretrofit;}@ProvidespublicUserApiService provideUserApiService(Retrofit retrofit){returnretrofit.create(UserApiService.class);}@ProvidespublicSharedPreferences provideSharedPreferences(Context context){returnPreferenceManager.getDefaultSharedPreferences(context);}@ProvidespublicUserManager provideUserManager(SharedPreferences preferences,UserApiService service){returnnewUserManager(preferences,service);}@ProvidespublicPasswordValidator providePasswordValidator(){returnnewPasswordValidator();}@ProvidespublicLoginPresenter provideLoginPresenter(UserManager userManager,PasswordValidator validator){returnnewLoginPresenter(userManager,validator);}} |
上面的程式碼如果你仔細看的話,會發現一個問題,那就是其中的SharedPreference provider方法 provideSharedPreferences(Context context)
需要一個context物件,但是 AppModule
裡面並沒有context 的Provider方法,這個怎麼辦呢?
對於這個問題,你可以再建立一個context provider方法,但是context物件從哪來呢?我們可以自定義一個Application,裡面提供一個靜態方法返回一個context,這種做法相信大家都幹過。Application類如下:
12345678910111213 | publicclassMyApplicationextendsApplication{privatestaticContext sContext;@OverridepublicvoidonCreate(){super.onCreate();sContext=this;}publicstaticContext getContext(){returnsContext;}} |
provider方法如下:
1234 | @ProvidespublicContext provideContext(){returnMyApplication.getContext();} |
但是這種方法不是很好,為什麼呢,因為context的獲得相當於是寫死了,只能從MyApplication.getContext(),如果測試環境下想把Context換成別的,還要給MyApplication定義一個setter,然後呼叫MyApplication.setContext(…),這個就繞的有點遠。更好的做法是,把Context作為 AppModule
的一個構造引數,從外面傳進來(應用DI模式,還記得嗎?):