注:
- 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的設計如下:
1 2 3 4 5 6 7 8 9 10 11 |
public class UserManager { private final SharedPreferences mPref; private final UserApiService mRestAdapter; public UserManager(SharedPreferences preferences, UserApiService userApiService) { this.mPref = preferences; this.mRestAdapter = userApiService; } /**Other code*/ } |
LoginPresenter的設計如下:
1 2 3 4 5 6 7 8 9 10 11 |
public class LoginPresenter { private final UserManager mUserManager; private final PasswordValidator mPasswordValidator; public LoginPresenter(UserManager userManager, PasswordValidator passwordValidator) { this.mUserManager = userManager; this.mPasswordValidator = passwordValidator; } /**Other code*/ } |
在這種情況下,最終的client LoginActivity裡面要new一個presenter,需要做的事情如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
public class LoginActivity extends AppCompatActivity { private LoginPresenter mLoginPresenter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); OkHttpClient okhttpClient = new OkHttpClient.Builder() .connectTimeout(30, TimeUnit.SECONDS) .build(); Retrofit retrofit = new Retrofit.Builder() .client(okhttpClient) .baseUrl("https://api.github.com") .build(); UserApiService userApiService = retrofit.create(UserApiService.class); SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); UserManager userManager = new UserManager(preferences, userApiService); PasswordValidator passwordValidator = new PasswordValidator(); mLoginPresenter = new LoginPresenter(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來說明:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
@Module public class AppModule { public OkHttpClient provideOkHttpClient() { OkHttpClient okhttpClient = new OkHttpClient.Builder() .connectTimeout(30, TimeUnit.SECONDS) .build(); return okhttpClient; } public Retrofit provideRetrofit(OkHttpClient okhttpClient) { Retrofit retrofit = new Retrofit.Builder() .client(okhttpClient) .baseUrl("https://api.github.com") .build(); return retrofit; } } |
在上面的Module(AppModule
)中,有兩個方法provideOkHttpClient()
和provideRetrofit(OkHttpClient okhttpClient)
,分別建立了兩個Dependency,OkHttpClient
和Retrofit
。但是呢,我們也說了,一個Module就是一個類,這個類有一些生產Dependency的方法,但它也可以有一些正常的,不是用來生產Dependency的方法。那怎麼樣讓管理員知道,一個Module裡面哪些方法是用來生產Dependency的,哪些不是呢?為了方便做這個區分,dagger2規定,所有生產Dependency的方法必須用 @Provides
這個annotation標註一下。所以,上面的 AppModule
正確的寫法應該是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
@Module public class AppModule { @Provides public OkHttpClient provideOkHttpClient() { OkHttpClient okhttpClient = new OkHttpClient.Builder() .connectTimeout(30, TimeUnit.SECONDS) .build(); return okhttpClient; } @Provides public Retrofit provideRetrofit(OkHttpClient okhttpClient) { Retrofit retrofit = new Retrofit.Builder() .client(okhttpClient) .baseUrl("https://api.github.com") .build(); return retrofit; } } |
這種用來生產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方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
@Module public class AppModule { @Provides public OkHttpClient provideOkHttpClient() { OkHttpClient okhttpClient = new OkHttpClient.Builder() .connectTimeout(30, TimeUnit.SECONDS) .build(); return okhttpClient; } @Provides public Retrofit provideRetrofit(OkHttpClient okhttpClient) { Retrofit retrofit = new Retrofit.Builder() .client(okhttpClient) .baseUrl("https://api.github.com") .build(); return retrofit; } @Provides public UserApiService provideUserApiService(Retrofit retrofit) { return retrofit.create(UserApiService.class); } @Provides public SharedPreferences provideSharedPreferences(Context context) { return PreferenceManager.getDefaultSharedPreferences(context); } @Provides public UserManager provideUserManager(SharedPreferences preferences, UserApiService service) { return new UserManager(preferences, service); } @Provides public PasswordValidator providePasswordValidator() { return new PasswordValidator(); } @Provides public LoginPresenter provideLoginPresenter(UserManager userManager, PasswordValidator validator) { return new LoginPresenter(userManager, validator); } } |
上面的程式碼如果你仔細看的話,會發現一個問題,那就是其中的SharedPreference provider方法 provideSharedPreferences(Context context)
需要一個context物件,但是 AppModule
裡面並沒有context 的Provider方法,這個怎麼辦呢?
對於這個問題,你可以再建立一個context provider方法,但是context物件從哪來呢?我們可以自定義一個Application,裡面提供一個靜態方法返回一個context,這種做法相信大家都幹過。Application類如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class MyApplication extends Application { private static Context sContext; @Override public void onCreate() { super.onCreate(); sContext = this; } public static Context getContext() { return sContext; } } |
provider方法如下:
1 2 3 4 |
@Provides public Context provideContext() { return MyApplication.getContext(); } |
但是這種方法不是很好,為什麼呢,因為context的獲得相當於是寫死了,只能從MyApplication.getContext(),如果測試環境下想把Context換成別的,還要給MyApplication定義一個setter,然後呼叫MyApplication.setContext(…),這個就繞的有點遠。更好的做法是,把Context作為 AppModule
的一個構造引數,從外面傳進來(應用DI模式,還記得嗎?):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
@Module public class AppModule { private final Context mContext; public AppModule(Context context) { this.mContext = context; } @Provides public Context provideContext() { return mContext; } //其他的provider方法 } |
是的,一個Module就是一個正常的類,它也可以有構造方法,以及其他正常類的特性。你可能會想那給建構函式的context物件從哪來呢?別急,這個問題馬上解答。
Dependency工廠管理員:Component
前面我們講了dagger2的一半,就是生產Dependency的工廠:Module。接下來我們講另一半,工廠管理員:Component。跟Module不同的是,我們在實現Component時,不是定義一個類,而是定義一個介面(interface):
1 2 |
public interface AppComponent { } |
名字可以隨便取,跟Module需要用 @Module
修飾一下類似的,一個dagger2的Component需要用 @Component
修飾一下,來標註這是一個dagger2的Component,而不是一個普通的interface,所以正確的定義方式是:
1 2 3 |
@Component public interface AppComponent { } |
在實際情況中,可能有多個Module,也可能有多個Component,那麼當Component接收到一個Client的Dependency請求時,它怎麼知道要從哪個Module裡面去找這些Dependency呢?它不可能遍歷我們的每一個類,然後找出所有的Module,再遍歷所有Module的Provider方法,去找Dependency,這樣先不說能不能做到,就算做得到,效率也太低了。因此dagger2規定,我們在定義Component的時候,必須指定這個管理員“管理”哪些工廠(Module)。指定的方法是,把需要這個Component管理的Module傳給 @Component
這個註解的modules屬性(或者叫方法?),如下:
1 2 3 |
@Component(modules = {AppModule.class}) // public interface AppComponent { } |
modules屬性接收一個陣列,裡面是這個Component管理的所有Module。在上面的例子中,AppComponent
只管理AppModule
一個。
Component給Client提供Dependency的方法
前面我們講了Module和Component的實現,接下來就是Component怎麼給Client提供Dependency的問題了。一般來說,有兩種,當然總共不止這兩種,只不過這兩種最常用,也最好理解,一般來說用這兩種就夠了,因此這裡不贅述其他的方法。
方法一:在Component裡面定義一個返回Dependency的方法
第一種是在Component裡面定義一個返回Dependency的方法,比如LoginActivity需要LoginPresenter,那麼我們可以在AppComponent
裡面定義一個返回LoginPresenter
的方法:
1 2 3 4 |
@Component(modules = {AppModule.class}) public interface AppComponent { LoginPresenter loginPresenter(); } |
你可能會好奇,為什麼Component只需要定義成介面就行了,不是應該定義一個類,然後自己使用Module去做這件事嗎?如果是這樣的話,那就太low了。dagger2的工作原理是,在你的java程式碼編譯成位元組碼的過程中,dagger2會對所有的Component(就是用 @Component
修飾過的interface)進行處理,自動生成一個實現了這個interface的類,生成的類名是Component的名字前面加上“Dagger”。比如我們定義的 AppComponent
,對應的自動生成的類叫做DaggerAppComponent
。我們知道,實現一個interface需要實現裡面的所有方法,因此,DaggerAppComponent
是實現了 loginPresenter();
這個方法的。實現的方式大致就是從 AppComponent
管理的 AppModule
裡面去找LoginPresenter
的Provider方法,然後呼叫這個方法,返回一個LoginPresenter
。
因此,使用這種方式,當Client需要Dependency的時候,首先需要用DaggerAppComponent
這個類建立一個物件,然後呼叫這個物件的 loginPresenter()
方法,這樣Client就能獲得一個LoginPresenter
了,這個DaggerAppComponent
物件的建立及使用方式如下:
1 2 3 4 5 6 7 8 9 10 11 12 |
public class LoginActivity extends AppCompatActivity { private LoginPresenter mLoginPresenter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); AppComponent appComponent = DaggerAppComponent.builder().appModule(new AppModule(this)).build(); // mLoginPresenter = appComponent.loginPresenter(); // } } |
總結一下,我們到現在為止,做了什麼:
- 我們定義了一個
AppModule
類,裡面定義了一些Provider方法 - 定義了一個
AppComponent
,裡面定義了一個返回LoginPresenter
的方法loginPresenter()
。
就這樣,我們便可以使用 DaggerAppComponent.builder().appModule(new AppModule(this)).build().loginPresenter();
來獲取一個LoginPresenter
物件了。
這簡直就是magic,不是嗎?
如果不是dagger2,而是我們自己來實現這個AppComponent
interface,想想我們需要做哪些事情:
- 定義一個Constructor,接受一個
AppModule
物件,儲存在field中(mAppModule) - 實現loginPresenter()方法,呼叫mAppModule的
provideLoginPresenter(UserManager userManager, PasswordValidator validator)
方法,這時候發現這個方法需要兩個引數UserManager
和PasswordValidator
。 - 呼叫
provideUserManager(SharedPreferences preferences, UserApiService service)
來獲取一個UserManager
,這時候發現這個方法又需要兩個引數SharedPreferences
和UserApiService
。 - 呼叫
provideSharedPreferences(Context context)
來獲取一個SharedPreference,這時候發現先要有一個context - 。。。
- 。。。
- 。。。
說白了,就是把文章開頭我們寫的那段程式碼又實現了一遍,而使用dagger2,我們就做了前面描述的兩件事而已,這裡面錯綜複雜的Dependency關係dagger2幫我們自動理清了,生成相應的程式碼,去呼叫相應的Provider方法,滿足這些依賴關係。
也許這裡舉得這個例子不足以讓你覺得有什麼大不了的,但是你要知道,一個正常的App,可不僅僅有一個Login page而已,稍微大點的App,Dependency都有幾百甚至上千個,對於伺服器程式來說,Dependency則更多。對於這點,大家可以去看Dagger2主要作者的這個視訊,他裡面提到了Google一個android app有3000行程式碼專門來管理Dependency,而一個Server app甚至有10萬行這樣的程式碼。
這個時候要去手動new這些dependency、並且要以正確的順序new出來,簡直會要人命。而且讓問題更加棘手的是,隨著app的演進需求的變更,Dependency之間的關係也在動態的變化。比如說UserManager
不再使用SharedPreference
,而是使用database,這個時候UserManager
的建構函式裡面少了一個SharedPreferences
,多了一個DatabaseHelper
這樣的東西。
那麼如果使用正常的方式管理Dependency,所有new UserManager
的地方都要改,而是用dagger2,你只需要在 AppModule
裡面新增一個DatabaseHelper Provider方法,同時把UserManager
的provider方法第一引數從SharedPreferences
改成DatabaseHelper
就好了,所有用到UserManager
的地方不需要做任何更改,LoginPresenter
不需要做任何更改,LoginActivity
不需要任何更改,這難道不是magic嗎?
說點題外話,這種把問題(我們這裡是依賴關係)描述出來,而不是把實現過程寫出來的程式設計風格叫Declarative programming,跟它對應的叫Imperative Programming,相對於後者,前者的優勢是:可讀性更高,side effect更少,可擴充套件性更高等等。這是一種程式設計風格,跟語言、框架無關。當然,有的語言或框架天生就能讓程式設計師更容易的使用這種style來程式設計。這方面最顯著的當屬Prolog,有興趣的可以去了解下,絕對mind-blowing!
對於Java或Android開發者來說,想讓我們的程式碼更加declarative,最好的方式是使用dagger2和RxJava。
方法二:Field Injection
話說回來,我們繼續介紹dagger2,前面我們介紹了Component給Client提供Dependency的第一種方式,接下來繼續介紹第二種方式,這種方式叫 Field injection 。這裡我們繼續用LoginActivity
的例子來說明,LoginActivity
需要一個LoginPresenter
。那麼使用這種方式的做法是,我們就在LoginActivity
裡面定義一個LoginPresenter
的field,這個field需要使用 @Inject
修飾一下:
1 2 3 4 5 6 7 8 9 10 |
public class LoginActivity extends AppCompatActivity { @Inject // LoginPresenter mLoginPresenter; // @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } } |
然後在onCreate()裡面,我們把DaggerAppComponent
物件建立出來,呼叫這個物件的inject方法,把LoginActivity
傳進去:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public class LoginActivity extends AppCompatActivity { @Inject LoginPresenter mLoginPresenter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); AppComponent appComponent = DaggerAppComponent.builder().appModule(new AppModule(this)).build(); // appComponent.inject(this); // //從此之後,mLoginPresenter就被例項化了 //mLoginPresenter.isLogin() } } |
當然,我們需要先在AppComponent
裡面定義一個inject(LoginActivity loginActivity)
方法:
1 2 3 4 |
@Component(modules = {AppModule.class}) public interface AppComponent { void inject(LoginActivity loginActivity); // } |
DaggerAppComponent
實現這個方法的方式是,去LoginActivity
裡面所有被 @Inject
修飾的field,然後呼叫 AppModule
相應的Provider方法,賦值給這個field。這裡需要注意的是,@Inject
field不能使private,不然dagger2找不到這個field。
通常來說,這種方式比第一種方式更簡單,程式碼也更簡潔。假設LoginActivity
還需要其他的Dependency,比如需要一個統計打點的Dependency(StatManager
),那麼你只需要在AppModule
裡面定義一個Provider方法,然後在LoginActivity
裡面宣告另外一個field就好了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public class LoginActivity extends AppCompatActivity { @Inject LoginPresenter mLoginPresenter; @Inject StatManager mStatManager; // @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); AppComponent appComponent = DaggerAppComponent.builder().appModule(new AppModule(this)).build(); appComponent.inject(this); } } |
無論有多少個@Inject field,都只需要呼叫一次appComponent.inject(this);
。用過了你就會覺得,恩,好爽!
不過,需要注意的一點是,這種方式不支援繼承,比如說LoginActivity
繼承自一個 BaseActivity
,而@Inject StatManager mStatManager;
是放在BaseActivity
裡面的,那麼在LoginActivity
裡面呼叫 appComponent.inject(this);
並不會讓BaseActivity
裡面的 mStatManager
得到例項化,你必須在 BaseActivity
裡面也呼叫一次appComponent.inject(this);
。
@Singleton和Constructor Injection
到這裡,Client從Component獲取Dependency的兩種方式就介紹完畢。但是這裡有個問題,那就是每次Client向Component索要一個Dependency,Component都會建立一個新的出來,這可能會導致資源的浪費,或者說很多時候不是我們想要的,比如說,SharedPreferences
、UserManager
、OkHttpClient
, Retrofit
這些都只需要一份就好了,不需要每次都建立一個instance,這個時候我們可以給這些Dependency的Provider方法加上@Singleton就好了。如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
@Module public class AppModule { @Provides @Singleton // public OkHttpClient provideOkHttpClient() { OkHttpClient okhttpClient = new OkHttpClient.Builder() .connectTimeout(30, TimeUnit.SECONDS) .build(); return okhttpClient; } //other method } |
這樣,當Client第一次請求一個OkHttpClient
,dagger2會建立一個instance,然後儲存下來,下一次Client再次請求一個OkHttpClient
是,dagger2會直接返回上次建立好的,而不用再次建立instance。這就相當於用一種更簡便、而且DI-able的方式實現了singleton模式。
這裡再給大家一個bonus,如果你不需要做單元測試,而只是使用dagger2來做DI,組織app的結構的話,其實AppModule
裡面的很多Provider方法是不需要定義的。比如說在這種情況下,LoginPresenter
的Provider方法 provideLoginPresenter(UserManager userManager, PasswordValidator validator)
就不需要定義,你只需要在定義LoginPresenter
的時候,給它的Constructor加上 @Inject
修飾一下:
1 2 3 4 5 6 7 8 9 10 11 12 |
public class LoginPresenter { private final UserManager mUserManager; private final PasswordValidator mPasswordValidator; @Inject public LoginPresenter(UserManager userManager, PasswordValidator passwordValidator) { this.mUserManager = userManager; this.mPasswordValidator = passwordValidator; } //other methods } |
dagger2會自動建立這個LoginPresenter
所需要的Dependency是它能夠提供的,所以會去Module裡面找到這個LoginPresenter
所需的Dependency,交給LoginPresenter
的Constructor,建立好這Dependency,交給Client。這其實也是Client通過Component使用Dependency的一種方式,叫 Constructor injection (上一篇文章也提到Constructor injection,不過稍微有點不同,注意區分一下)同樣的,在那種情況下,UserManager
的Provider方法也不需要定義,而只需要給UserManager
的Constructor加上一個@Inject
就好了。說白了,你只需要給那些不是通過Constructor來建立的Dependency(比如說SharedPreferences、UserApiService等)定義Provider方法。
有了 Constructor injection ,我們的程式碼又能得到進一步的簡化,然而遺憾的是,這種方式將導致我們做單元測試的時候無法mock這中間的Dependency。說到單元測試,我們別忘了這個系列的主題T_T。。。那麼接下來就介紹dagger2在單元測試裡面的使用,以及為什麼 Constructor injection 將導致單元測試裡面無法mock這個Dependency。
dagger2在單元測試裡面的使用
在介紹dagger2在單元測試裡面的使用之前,我們先改進一下前面的程式碼,我們建立DaggerAppComponent
的地方是在LoginActivity
,其實這樣不是很好,為什麼呢?想想如果login以後其他地方也需要UserManager
,那麼我們又要建立一個DaggerAppComponent
,這種地方是很多的,畢竟 AppModule
裡面定義了一些整個app都要用到的Dependency,比如說Retrofit、SharedPreferences等等。如果每個需要用到的地方都建立一遍DaggerAppComponent
,就導致了程式碼的重複和記憶體效能的浪費。
理論上來說,DaggerAppComponent
物件整個app只需要一份就好了。所以我們在用dagger2的時候,一般的做法是,我們會在app啟動的時候建立好,放在某一個地方。比如,我們在自定義的Application#onCreate()裡面建立好,然後放在某個地方,我個人習慣定義一個類叫ComponentHolder,然後放裡面:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
public class MyApplication extends Application { @Override public void onCreate() { super.onCreate(); AppComponent appComponent = DaggerAppComponent.builder() .appModule(new AppModule(this)) .build(); ComponentHolder.setAppComponent(appComponent); } } public class ComponentHolder { private static AppComponent sAppComponent; public static void setAppComponent(AppComponent appComponent) { sAppComponent = appComponent; } public static AppComponent getAppComponent() { return sAppComponent; } } |
然後在需要 AppComponent
的地方,使用 ComponentHolder.getAppComponent()來獲取一個DaggerAppComponent
物件:
1 2 3 4 5 6 7 8 9 10 11 12 |
public class LoginActivity extends AppCompatActivity { @Inject LoginPresenter mLoginPresenter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ComponentHolder.getAppComponent().inject(this); // } } |
這樣在用的地方,看起來程式碼也乾淨了很多。
到這裡,我們就可以介紹在單元測試裡面怎麼來mock Dependency了。假設LoginActivity有兩個EditText和一個login button,點選這個button,將從兩個EditText裡面獲取使用者名稱和密碼,然後呼叫LoginPresenter
的login方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public class LoginActivity extends AppCompatActivity { @Inject LoginPresenter mLoginPresenter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ComponentHolder.getAppComponent().inject(this); findViewById(R.id.login).setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { String username = ((EditText) findViewById(R.id.username)).getText().toString(); String password = ((EditText) findViewById(R.id.password)).getText().toString(); mLoginPresenter.login(username, password); } }); } } |
我們現在要測的,就是當使用者點選這個login button的時候,mLoginPresenter
的login方法得到了呼叫,如果你看了這個系列的前面幾篇文章,你就知道這裡的mLoginPresenter
需要mock掉。但是,這裡的mLoginPresenter
是從dagger2的component裡面獲取的,這裡怎麼把mLoginPresenter
換成mock呢?
我們在回顧一下,其實LoginActivity
只是向DaggerAppComponent
索取了一個LoginPresenter
,而DaggerAppComponent
其實是呼叫了AppModule
的 provideLoginPresenter()
方法來獲得了一個LoginPresenter
,返回給LoginActivity
,也就是說,真正生產LoginPresenter
的地方是在 AppModule
。還記得嗎,我們建立DaggerAppComponent
的時候,給它的builder傳遞了一個AppModule
物件:
1 2 3 4 5 6 7 8 9 10 11 12 |
public class MyApplication extends Application { @Override public void onCreate() { super.onCreate(); AppComponent appComponent = DaggerAppComponent.builder() .appModule(new AppModule(this)) // .build(); ComponentHolder.setAppComponent(appComponent); } } |
其實DaggerAppComponent
呼叫的AppModule
物件,就是我們在建立它的時候傳給那個builder的。那麼,如果我們傳給DaggerAppComponent
的AppModule
是一個mock物件,在這個mock物件的provideLoginPresenter()被呼叫的時候,返回一個mock的LoginPresenter
,那麼LoginActivity
獲得的,不就是一個mock的LoginPresenter
了嗎?
我們用程式碼來實現一下看看是什麼樣子,這裡因為LoginActivity
是android相關的類,因此需要用到robolectric這個framework,雖然這個我們還沒有介紹到,但是程式碼應該看得懂,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
@RunWith(RobolectricGradleTestRunner.class) //Robolectric相關,看不懂的話忽略 @Config(constants = BuildConfig.class, sdk = 21) //同上 public class LoginActivityTest { @ Test public void testActivityStart() { @ Test public void testActivityStart() { AppModule mockAppModule = spy(new AppModule(RuntimeEnvironment.application)); //建立一個mockAppModule,這裡不能spy(AppModule.class),因為`AppModule`沒有預設無引數的Constructor,也不能mock(AppModule.class),原因是dagger2的約束,Provider方法不能返回null,除非用@Nullable修飾 LoginPresenter mockLoginPresenter = mock(LoginPresenter.class); //建立一個mockLoginPresenter Mockito.when(mockAppModule.provideLoginPresenter(any(UserManager.class), any(PasswordValidator.class))).thenReturn(mockLoginPresenter); //當mockAppModule的provideLoginPresenter()方法被呼叫時,讓它返回mockLoginPresenter AppComponent appComponent = DaggerAppComponent.builder().appModule(mockAppModule).build(); //用mockAppModule來建立DaggerAppComponent ComponentHolder.setAppComponent(appComponent); //記得放到ComponentHolder裡面,這樣LoginActivity#onCreate()裡面通過ComponentHolder.getAppComponent()獲得的就是這裡建立的appComponent LoginActivity loginActivity = Robolectric.setupActivity(LoginActivity.class); //啟動LoginActivity,onCreate方法會得到呼叫,裡面的mLoginPresenter通過dagger2獲得的,將是mockLoginPresenter ((EditText) loginActivity.findViewById(R.id.username)).setText("xiaochuang"); ((EditText) loginActivity.findViewById(R.id.password)).setText("xiaochuang is handsome"); loginActivity.findViewById(R.id.login).performClick(); verify(mockLoginPresenter).login("xiaochuang", "xiaochuang is handsome"); //pass! } } } |
這就是dagger2在單元測試裡面的應用。基本上就是mock Module的Provider方法,讓它返回你想要的mock物件。這也解釋了為什麼說只用 Constructor injection 的話,會導致Dependency無法mock,因為沒有對應的Provider方法來讓我們mock啊。上面的程式碼看起來也許你會覺得有點多,然而實際開發中,上面測試方法裡的第1、4、5行都是通用的,我們可以把他們抽到一個輔助類裡面:
1 2 3 4 5 6 7 8 |
public class TestUtils { public static final AppModule appModule = spy(new AppModule(RuntimeEnvironment.application)); public static void setupDagger() { AppComponent appComponent = DaggerAppComponent.builder().appModule(appModule).build(); ComponentHolder.setAppComponent(appComponent); } } |
這樣我們前面的測試方法就可以簡化了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public class LoginActivityTest { @ Test public void testActivityStart() { TestUtils.setupDagger(); LoginPresenter mockLoginPresenter = mock(LoginPresenter.class); Mockito.when(TestUtils.appModule.provideLoginPresenter(any(UserManager.class), any(PasswordValidator.class))).thenReturn(mockLoginPresenter); LoginActivity loginActivity = Robolectric.setupActivity(LoginActivity.class); ((EditText) loginActivity.findViewById(R.id.username)).setText("xiaochuang"); ((EditText) loginActivity.findViewById(R.id.password)).setText("xiaochuang is handsome"); loginActivity.findViewById(R.id.login).performClick(); verify(mockLoginPresenter).login("xiaochuang", "xiaochuang is handsome"); } } |
當然,上面的程式碼還可以用很多種方法作進一步簡化,比如把TestUtils.setupDagger();
放到@Before
裡面,或者是自定義一個基礎測試類,把TestUtils.setupDagger();
放這個基礎測試類的@Before
裡面,然後 LoginActivityTest
繼承這個基礎測試類就可以了,or even better,自定義一個JUnit Rule,在每個測試方法被呼叫之前自動呼叫 TestUtils.setupDagger();
。只是這些與當前的主題無關,就不具體展開敘述了。後面會講到DaggerMock的使用,這個東西可真的是神器啊!簡直不要太神器!
單元測試裡面,不要濫用dagger2
這裡再重複一下上一篇文章的話,單元測試的時候不要濫用dagger2,雖然現在我們的app是用dagger2架構起來的,所有的Dependency都是在Module裡面生產,但並不代表我們在做單元測試的時候,這些Dependency也只能在Module裡面生產。比如說,LoginPresenter
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public class LoginPresenter { private final UserManager mUserManager; private final PasswordValidator mPasswordValidator; @Inject public LoginPresenter(UserManager userManager, PasswordValidator passwordValidator) { this.mUserManager = userManager; this.mPasswordValidator = passwordValidator; } public void login(String username, String password) { if (username == null || username.length() == 0) return; if (mPasswordValidator.verifyPassword(password)) return; mUserManager.performLogin(username, password); } } |
我們要測的是,LoginPresenter#login()
呼叫了 mUserManager.performLogin()
。在這裡,我們可以按照上面的思路,使用dagger2來mock UserManager
,做法是mock module 的provideUserManager()
方法,讓它返回一個mock的 UserManager
,然後去verify這個mock UserManager
的performLogin()
方法得到了呼叫,程式碼大致如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
@RunWith(RobolectricGradleTestRunner.class) @Config(constants = BuildConfig.class, sdk = 21) public class LoginPresenterTest { @ Test public void testLogin() throws Exception { TestUtils.setupDagger(); UserManager mockUserManager = mock(UserManager.class); Mockito.when(TestUtils.appModule.provideUserManager(any(SharedPreferences.class), any(UserApiService.class))).thenReturn(mockUserManager); LoginPresenter presenter = ComponentHolder.getAppComponent().loginPresenter(); presenter.login("xiaochuang", "xiaochuang is handsome"); verify(mockUserManager).performLogin("xiaochuang", "xiaochuang is handsome"); } } |
這樣雖然可以,而且也不難,但畢竟路繞的有點遠,而且你可能要做額外的一些工作,比如在AppComponent
裡面加一個正式程式碼不一定會用的 loginPresenter();
方法,另外因為AppModule
裡面有安卓相關的程式碼,我們還必須使用Robolectric,導致測試跑起來慢了很多。其實我們完全可以不用dagger2,有更好的辦法,那就是直接new LoginPresenter
,傳入mock UserManager
:
1 2 3 4 5 6 7 8 9 10 11 |
public class LoginPresenterTest { @ Test public void testLogin() { UserManager mockUserManager = mock(UserManager.class); LoginPresenter presenter = new LoginPresenter(mockUserManager, new PasswordValidator()); //因為這裡我們不verify PasswordValidator,所以不需要mock這個。 presenter.login("xiaochuang", "xiaochuang is handsome"); verify(mockUserManager).performLogin("xiaochuang", "xiaochuang is handsome"); } } |
程式是不是簡單多了,也容易理解多了?
那麼現在問題來了,如果這樣的話,單元測試的時候,哪些情況應該用dagger2,那些情況不用呢?答案是,能不用dagger2,就不用dagger2,不得已用dagger2,才用dagger2。當然,這是一句廢話,前面我們已經明顯感受到了,在單元測試裡面用dagger2比不用dagger2要麻煩多了,能不用當然不用。那麼問題就變成了,什麼情況下必須用dagger2、而什麼時候可以不用呢?答案是,如果被測類(比如說LoginActivity
)的Dependency(LoginPresenter
)是通過 field injection inject進去的,那麼再測這個類(LoginActivity
)的時候,就必須用dagger2,不然很難優雅的把mock傳進去。相反,如果被測類有Constructor(比如說LoginPresenter
),Dependency是通過Constructor傳進去的,那麼就可以不使用dagger2,而是直接new物件出來測。這也是為什麼我在前一篇文章裡面強烈的推薦 Constructor Injection的原因。
小結
這篇文章介紹了dagger2的使用,以及在單元測試裡面的應用。哦好像忘了介紹把dagger2加到專案裡面的方法,其實很簡單,把以下程式碼加入build.gradle:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
buildscript { repositories { jcenter() } dependencies { classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8' } } apply plugin: 'com.android.application' //這個已經有了,這裡只是想說明要把android-apt這個plugin放到這個的後面。 apply plugin: 'com.neenbedankt.android-apt' dependencies { //other dependencies //Dagger2 compile 'com.google.dagger:dagger:2.0.2' compile 'javax.annotation:jsr250-api:1.0' apt 'com.google.dagger:dagger-compiler:2.0.2' } |
應該說,DI是一種很好的模式,哪怕不做單元測試,DI也會讓我們的app的架構變得乾淨很多,可讀性、維護性和可擴充性強很多,只不過單元測試讓DI的必要性變得更加顯著和迫切而已。而dagger2的作用,或者說角色,在於它讓我們寫正式程式碼的時候使用DI變得易如反掌,程式及其簡潔優雅可讀性高。同時,它在某些情況下讓原來很難測的程式碼變得用以測試。
文中的程式碼在github這個專案裡面。
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!