1. 程式人生 > >Android單元測試(6):使用dagger2來做依賴注入

Android單元測試(6):使用dagger2來做依賴注入

注:

  • 1. 程式碼中的 //<= 表示新加的、修改的等需要重點關注的程式碼
  • 2. Class#method表示一個類的instance method,比如 LoginPresenter#login 表示 LoginPresenter的login(非靜態)方法。

問題

前一篇文章中,我們講述了依賴注入的概念,以及依賴注入對單元測試極其關鍵的重要性和必要性。在那篇文章的結尾,我們遇到了一個問題,那就是如果不使用DI框架,而全部採用手工來做DI的話,那麼所有的Dependency都需要在最上層的client來生成,這可不是件好事情。繼續用我們前面的例子來具體說明一下。

假設有一個登入介面,LoginActivity

,他有一個LoginPresenterLoginPresenter用到了UserManagerPasswordValidator,為了讓問題變得更明顯一點,我們假設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的其他地方也需要這裡的OkHttpClientRetrofitSharedPreferenceUserManager等等dependency,因此也需要new這些東西出來,造成大量的程式碼重複,和不必要的object instance生成。然而如前所述,我們又必須用到DI模式,這個怎麼辦呢?

想想,如果能達到這樣的效果,那該有多好:我們只需要在一個類似於dependency工廠的地方統一生產這些dependency,以及這些dependency的dependency。所有需要用到這些Dependency的client都從這個工廠裡面去獲取。而且更妙的是,一個client(比如說LoginActivity)只需要知道它直接用到的Dependency(LoginPresenter),而不需要知道它的Dependency(LoginPresenter)又用到哪些Dependency(UserManagerPasswordValidator)。系統自動識別出這個依賴關係,從工廠裡面把需要的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,OkHttpClientRetrofit。但是呢,我們也說了,一個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模式,還記得嗎?):