- 原文地址:Dependency Injection with Dagger 2
- 原文作者:CodePath
- 譯文出自:掘金翻譯計劃
- 譯者: tanglie1993
- 校對者:mnikn, Zhiw
用 Dagger 2 實現依賴注入
概要
很多 Android 應用依賴於一些含有其它依賴的物件。例如,一個 Twitter API 客戶端可能需要通過 Retrofit 之類的網路庫來構建。要使用這個庫,你可能還需要新增 Gson 這樣的解析庫。另外,實現認證或快取的庫可能需要使用 shared preferences 或其它通用儲存方式。這就需要先把它們例項化,並建立一個隱含的依賴鏈。
如果你不熟悉依賴注入,看看這個短視訊。
Dagger 2 為你解析這些依賴,並生成把它們繫結在一起的程式碼。也有很多其它的 Java 依賴注入框架,但它們中大多數是有缺陷的,比如依賴 XML,需要在執行時驗證依賴,或者在起始時造成效能負擔。 Dagger 2 純粹依賴於 Java 註解解析器以及編譯時檢查來分析並驗證依賴。它被認為是目前最高效的依賴注入框架之一。
優點
這是使用 Dagger 2 的一系列其它優勢:
- 簡化共享例項訪問。就像 ButterKnife 庫簡化了引用View, event handler 和 resources 的方式一樣,Dagger 2 提供了一個簡單的方式獲取對共享物件的引用。例如,一旦我們在 Dagger 中宣告瞭
MyTwitterApiClient
或SharedPreferences
的單例,就可以用一個簡單的@Inject
標註來宣告域:
public class MainActivity extends Activity {
@Inject MyTwitterApiClient mTwitterApiClient;
@Inject SharedPreferences sharedPreferences;
public void onCreate(Bundle savedInstance) {
// assign singleton instances to fields
InjectorClass.inject(this);
}複製程式碼
容易配置複雜的依賴關係。 物件建立是有隱含順序的。Dagger 2 遍歷依賴關係圖,並且生成易於理解和追蹤的程式碼。而且,它可以節約大量的樣板程式碼,使你不再需要手寫,手動獲取引用並把它們傳遞給其他物件作為依賴。它也簡化了重構,因為你可以聚焦於構建模組本身,而不是它們被建立的順序。
更簡單的單元和整合測試 因為依賴圖是為我們建立的,我們可以輕易換出用於建立網路響應的模組,並模擬這種行為。
例項範圍 你不僅可以輕易地管理持續整個應用生命週期的例項,也可以利用 Dagger 2 來定義生命週期更短(比如和一個使用者 session 或 Activity 生命週期相繫結)的例項。
設定
預設的 Android Studio 不把生成的 Dagger 2 程式碼視作合法的類,因為它們通常並不被加入 source 路徑。但引入 android-apt
外掛後,它會把這些檔案加入 IDE classpath,從而提供更好的可見性。
確保升級 到最新的 Gradle 版本以使用最新的 annotationProcessor
語法:
dependencies {
// apt command comes from the android-apt plugin
compile "com.google.dagger:dagger:2.9"
annotationProcessor "com.google.dagger:dagger-compiler:2.9"
provided 'javax.annotation:jsr250-api:1.0'
}複製程式碼
注意 provided
關鍵詞是指只在編譯時需要的依賴。Dagger 編譯器生成了用於生成依賴圖的類,而這個依賴圖是在你的原始碼中定義的。這些類在編譯過程中被新增到你的IDE classpath。annotationProcessor
關鍵字可以被 Android Gradle 外掛理解。它不把這些類新增到 classpath 中,而只是把它們用於處理註解。這可以避免不小心引用它們。
建立單例
最簡單的例子是用 Dagger 2 集中管理所有的單例。假設你不用任何依賴注入框架,在你的 Twitter 客戶端中寫下類似這些的東西:
OkHttpClient client = new OkHttpClient();
// Enable caching for OkHttp
int cacheSize = 10 * 1024 * 1024; // 10 MiB
Cache cache = new Cache(getApplication().getCacheDir(), cacheSize);
client.setCache(cache);
// Used for caching authentication tokens
SharedPreferences sharedPrefeences = PreferenceManager.getDefaultSharedPreferences(this);
// Instantiate Gson
Gson gson = new GsonBuilder().create();
GsonConverterFactory converterFactory = GsonConverterFactory.create(gson);
// Build Retrofit
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com")
.addConverterFactory(converterFactory)
.client(client) // custom client
.build();複製程式碼
宣告你的單例
你需要通過建立 Dagger 2 模組定義哪些物件應該作為依賴鏈的一部分。例如,假設我們想要建立一個 Retrofit
單例,使它繫結到應用生命週期,對所有的 Activity 和 Fragment 都可用,我們首先需要使 Dagger 意識到他可以提供 Retrofit
的例項。
因為需要設定快取,我們需要一個 Application context。我們的第一個 Dagger 模組,AppModule.java
,被用於提供這個依賴。我們將定義一個 @Provides
註解,標註帶有 Application
的構造方法:
@Module
public class AppModule {
Application mApplication;
public AppModule(Application application) {
mApplication = application;
}
@Provides
@Singleton
Application providesApplication() {
return mApplication;
}
}複製程式碼
我們建立了一個名為 NetModule.java
的類,並用 @Module
來通知 Dagger,在這裡查詢提供例項的方法。
返回例項的方法也應當用 @Provides
標註。Singleton
標註通知 Dagger 編譯器,例項在應用中只應被建立一次。在下面的例子中,我們把 SharedPreferences
, Gson
, Cache
, OkHttpClient
, 和 Retrofit
設定為在依賴列表中可用的型別。
@Module
public class NetModule {
String mBaseUrl;
// Constructor needs one parameter to instantiate.
public NetModule(String baseUrl) {
this.mBaseUrl = baseUrl;
}
// Dagger will only look for methods annotated with @Provides
@Provides
@Singleton
// Application reference must come from AppModule.class
SharedPreferences providesSharedPreferences(Application application) {
return PreferenceManager.getDefaultSharedPreferences(application);
}
@Provides
@Singleton
Cache provideOkHttpCache(Application application) {
int cacheSize = 10 * 1024 * 1024; // 10 MiB
Cache cache = new Cache(application.getCacheDir(), cacheSize);
return cache;
}
@Provides
@Singleton
Gson provideGson() {
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES);
return gsonBuilder.create();
}
@Provides
@Singleton
OkHttpClient provideOkHttpClient(Cache cache) {
OkHttpClient client = new OkHttpClient();
client.setCache(cache);
return client;
}
@Provides
@Singleton
Retrofit provideRetrofit(Gson gson, OkHttpClient okHttpClient) {
Retrofit retrofit = new Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create(gson))
.baseUrl(mBaseUrl)
.client(okHttpClient)
.build();
return retrofit;
}
}複製程式碼
注意,方法名稱(比如 provideGson()
, provideRetrofit()
等)是沒關係的,可以任意設定。@Provides
被用於把這個例項化和其它同類的模組聯絡起來。@Singleton
標註用於通知 Dagger,它在整個應用的生命週期中只被初始化一次。
一個 Retrofit
例項依賴於一個 Gson
和一個 OkHttpClient
例項,所以我們可以在同一個類中定義兩個方法,來提供這兩種例項。@Provides
標註和方法中的這兩個引數將使 Dagger 意識到,構建一個 Retrofit
例項 需要依賴 Gson
和 OkHttpClient
。
定義注入目標
Dagger 使你的 activity, fragment, 或 service 中的域可以通過 @Inject
註解和呼叫 inject()
方法被賦值。呼叫 inject()
將會使得 Dagger 2 在依賴圖中尋找合適型別的單例。如果找到了一個,它就把引用賦值給對應的域。例如,在下面的例子中,它會嘗試找到一個返回MyTwitterApiClient
和SharedPreferences
型別的 provider:
public class MainActivity extends Activity {
@Inject MyTwitterApiClient mTwitterApiClient;
@Inject SharedPreferences sharedPreferences;
public void onCreate(Bundle savedInstance) {
// assign singleton instances to fields
InjectorClass.inject(this);
}複製程式碼
Dagger 2 中使用的注入者類被稱為 component。它把先前定義的單例的引用傳給 activity, service 或 fragment。我們需要用 @Component
來註解這個類。注意,需要被注入的 activity, service 或 fragment 需要在這裡使用 inject()
方法注入:
@Singleton
@Component(modules={AppModule.class, NetModule.class})
public interface NetComponent {
void inject(MainActivity activity);
// void inject(MyFragment fragment);
// void inject(MyService service);
}複製程式碼
注意 基類不能被作為注入的目標。Dagger 2 依賴於強型別的類,所以你必須指定哪些類會被定義。(有一些建議 幫助你繞開這個問題,但這樣做的話,程式碼可能會變得更復雜,更難以追蹤。)
生成程式碼
Dagger 2 的一個重要特點是它會為標註 @Component
的介面生成類的程式碼。你可以使用帶有 Dagger
(比如 DaggerTwitterApiComponent.java
) 字首的類來為依賴圖提供例項,並用它來完成用 @Inject
註解的域的注入。 參見設定。
例項化元件
我們應該在一個 Application
類中完成這些工作,因為這些例項應當在 application 的整個週期中只被宣告一次:
public class MyApp extends Application {
private NetComponent mNetComponent;
@Override
public void onCreate() {
super.onCreate();
// Dagger%COMPONENT_NAME%
mNetComponent = DaggerNetComponent.builder()
// list of modules that are part of this component need to be created here too
.appModule(new AppModule(this)) // This also corresponds to the name of your module: %component_name%Module
.netModule(new NetModule("https://api.github.com"))
.build();
// If a Dagger 2 component does not have any constructor arguments for any of its modules,
// then we can use .create() as a shortcut instead:
// mNetComponent = com.codepath.dagger.components.DaggerNetComponent.create();
}
public NetComponent getNetComponent() {
return mNetComponent;
}
}複製程式碼
如果你不能引用 Dagger 元件,rebuild 整個專案 (在 Android Studio 中,選擇 Build > Rebuild Project)。
因為我們在覆蓋預設的 Application
類,我們同樣需要修改應用的 name
以啟動 MyApp
。這樣,你的 application 將會使用這個 application 類來處理最初的例項化。
<application
android:allowBackup="true"
android:name=".MyApp">複製程式碼
在我們的 activity 中,我們只需要獲取這些 components 的引用,並呼叫 inject()
。
public class MyActivity extends Activity {
@Inject OkHttpClient mOkHttpClient;
@Inject SharedPreferences sharedPreferences;
public void onCreate(Bundle savedInstance) {
// assign singleton instances to fields
// We need to cast to `MyApp` in order to get the right method
((MyApp) getApplication()).getNetComponent().inject(this);
}複製程式碼
限定詞型別
如果我們需要同一型別的兩個不同物件,我們可以使用 @Named
限定詞註解。 你需要定義你如何提供單例 (用 @Provides
註解),以及你從哪裡注入它們(用 @Inject
註解):
@Provides @Named("cached")
@Singleton
OkHttpClient provideOkHttpClient(Cache cache) {
OkHttpClient client = new OkHttpClient();
client.setCache(cache);
return client;
}
@Provides @Named("non_cached") @Singleton
OkHttpClient provideOkHttpClient() {
OkHttpClient client = new OkHttpClient();
return client;
}複製程式碼
注入同樣需要這些 named 註解:
@Inject @Named("cached") OkHttpClient client;
@Inject @Named("non_cached") OkHttpClient client2;複製程式碼
@Named
是一個被 Dagger 預先定義的限定語,但你也可以建立你自己的限定語註解:
@Qualifier
@Documented
@Retention(RUNTIME)
public @interface DefaultPreferences {
}複製程式碼
作用域
在 Dagger 2 中,你可以通過自定義作用域來定義元件應當如何封裝。例如,你可以建立一個只持續 activity 或 fragment 整個生命週期的作用域。你也可以建立一個對應一個使用者認證 session 的作用域。 你可以定義任意數量的自定義作用域註解,只要你把它們宣告為 public @interface
:
@Scope
@Documented
@Retention(value=RetentionPolicy.RUNTIME)
public @interface MyActivityScope
{
}複製程式碼
雖然 Dagger 2 在執行時不依賴註解,把 RetentionPolicy
設定為 RUNTIME 對於將來檢查你的 module 將是很有用的。
依賴元件和子元件
利用作用域,我們可以建立 依賴元件 或 子元件。上面的例子中,我們使用了 @Singleton
註解,它持續了整個應用的生命週期。我們也依賴了一個主要的 Dagger 元件。
如果我們不需要元件總是存在於記憶體中(例如,和 activity 或 fragment 生命週期繫結,或在使用者登入時繫結),我們可以建立依賴元件和子元件。它們各自提供了一種封裝你的程式碼的方式。我們將在下一節中看到如何使用它們。
在使用這種方法時,有若干問題要注意:
- 依賴元件需要父元件顯式指定哪些依賴可以在下游注入,而子元件不需要 對父元件而言,你需要通過指定型別和方法來向下遊元件暴露這些依賴:
// parent component
@Singleton
@Component(modules={AppModule.class, NetModule.class})
public interface NetComponent {
// remove injection methods if downstream modules will perform injection
// downstream components need these exposed
// the method name does not matter, only the return type
Retrofit retrofit();
OkHttpClient okHttpClient();
SharedPreferences sharedPreferences();
}複製程式碼
如果你忘記加入這一行,你將有可能看到一個關於注入目標缺失的錯誤。就像 private/public 變數的管理方式一樣,使用一個 parent 元件可以更顯式地控制,也可保證更好的封裝。使用子元件使得依賴注入更容易管理,但封裝得更差。
兩個依賴元件不能使用同一個作用域 例如,兩個元件不能都用
@Singleton
註解設定定義域。這個限制的原因在 這裡 有所說明。依賴元件需要定義它們自己的作用域。Dagger 2 同樣允許使用帶作用域的例項。你需要負責在合適的時機建立和銷燬引用。 Dagger 2 對底層實現一無所知。這個 Stack Overflow 討論 上有更多的細節。
依賴元件
如果你想要建立一個元件,使它的生命週期和已登入使用者的 session 相繫結,就可以建立 UserScope
介面:
import java.lang.annotation.Retention;
import javax.inject.Scope;
@Scope
public @interface UserScope {
}複製程式碼
接下來,我們定義父元件:
@Singleton
@Component(modules={AppModule.class, NetModule.class})
public interface NetComponent {
// downstream components need these exposed with the return type
// method name does not really matter
Retrofit retrofit();
}複製程式碼
接下來定義子元件:
@UserScope // using the previously defined scope, note that @Singleton will not work
@Component(dependencies = NetComponent.class, modules = GitHubModule.class)
public interface GitHubComponent {
void inject(MainActivity activity);
}複製程式碼
假定 Github 模組只是把 API 介面返回給 Github API:
@Module
public class GitHubModule {
public interface GitHubApiInterface {
@GET("/org/{orgName}/repos")
Call<ArrayList<Repository>> getRepository(@Path("orgName") String orgName);
}
@Provides
@UserScope // needs to be consistent with the component scope
public GitHubApiInterface providesGitHubInterface(Retrofit retrofit) {
return retrofit.create(GitHubApiInterface.class);
}
}複製程式碼
為了讓這個 GitHubModule.java
獲得對 Retrofit
例項的引用,我們需要在上游元件中顯式定義它們。如果下游模組會執行注入,它們也應當被從上游元件中移除:
@Singleton
@Component(modules={AppModule.class, NetModule.class})
public interface NetComponent {
// remove injection methods if downstream modules will perform injection
// downstream components need these exposed
Retrofit retrofit();
OkHttpClient okHttpClient();
SharedPreferences sharedPreferences();
}複製程式碼
最終的步驟是用 GitHubComponent
進行例項化。這一次,我們需要首先實現 NetComponent
並把它傳遞給 DaggerGitHubComponent
builder 的構造方法:
NetComponent mNetComponent = DaggerNetComponent.builder()
.appModule(new AppModule(this))
.netModule(new NetModule("https://api.github.com"))
.build();
GitHubComponent gitHubComponent = DaggerGitHubComponent.builder()
.netComponent(mNetComponent)
.gitHubModule(new GitHubModule())
.build();複製程式碼
示例程式碼 中有一個實際的例子。
子元件
使用子元件是擴充套件元件物件圖的另一種方式。就像帶有依賴的元件一樣,子元件有自己的的生命週期,而且在所有對子元件的引用都失效之後,可以被垃圾回收。此外它們作用域的限制也一樣。使用這個方式的一個優點是你不需要定義所有的下游元件。
另一個主要的不同是,子元件需要在父元件中宣告。
這是為一個 activity 使用子元件的例子。我們用自定義作用域和 @Subcomponent
註解這個類:
@MyActivityScope
@Subcomponent(modules={ MyActivityModule.class })
public interface MyActivitySubComponent {
@Named("my_list") ArrayAdapter myListAdapter();
}複製程式碼
被使用的模組在下面定義:
@Module
public class MyActivityModule {
private final MyActivity activity;
// must be instantiated with an activity
public MyActivityModule(MyActivity activity) { this.activity = activity; }
@Provides @MyActivityScope @Named("my_list")
public ArrayAdapter providesMyListAdapter() {
return new ArrayAdapter<String>(activity, android.R.layout.my_list);
}
...
}複製程式碼
最後,在父元件中,我們將定義一個工廠方法,它以這個元件的型別作為返回值,並定義初始化所需的依賴:
@Singleton
@Component(modules={ ... })
public interface MyApplicationComponent {
// injection targets here
// factory method to instantiate the subcomponent defined here (passing in the module instance)
MyActivitySubComponent newMyActivitySubcomponent(MyActivityModule activityModule);
}複製程式碼
在上面的例子中,一個子元件的新例項將在每次 newMyActivitySubcomponent()
呼叫時被建立。把這個子模組注入一個 activity 中:
public class MyActivity extends Activity {
@Inject ArrayAdapter arrayAdapter;
public void onCreate(Bundle savedInstance) {
// assign singleton instances to fields
// We need to cast to `MyApp` in order to get the right method
((MyApp) getApplication()).getApplicationComponent())
.newMyActivitySubcomponent(new MyActivityModule(this))
.inject(this);
}
}複製程式碼
子元件 builder
從 v2.7 版本起可用
子元件 builder 使建立子元件的類和子元件的父類解耦。這是通過移除父元件中的子元件工廠方法實現的。
@MyActivityScope
@Subcomponent(modules={ MyActivityModule.class })
public interface MyActivitySubComponent {
...
@Subcomponent.Builder
interface Builder extends SubcomponentBuilder<MyActivitySubComponent> {
Builder activityModule(MyActivityModule module);
}
}
public interface SubcomponentBuilder<V> {
V build();
}複製程式碼
子元件是在子元件介面內部的介面中宣告的。它必須含有一個 build()
方法,其返回值和子元件相匹配。用這個方法宣告一個基介面是很方便的,就像上面的SubcomponentBuilder
一樣。這個新的 builder 必須被加入父元件的圖中,而這是用一個 "binder" 模組和一個 "subcomponents" 引數實現的:
@Module(subcomponents={ MyActivitySubComponent.class })
public abstract class ApplicationBinders {
// Provide the builder to be included in a mapping used for creating the builders.
@Binds @IntoMap @SubcomponentKey(MyActivitySubComponent.Builder.class)
public abstract SubcomponentBuilder myActivity(MyActivitySubComponent.Builder impl);
}
@Component(modules={..., ApplicationBinders.class})
public interface ApplicationComponent {
// Returns a map with all the builders mapped by their class.
Map<Class<?>, Provider<SubcomponentBuilder>> subcomponentBuilders();
}
// Needed only to to create the above mapping
@MapKey @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME)
public @interface SubcomponentKey {
Class<?> value();
}複製程式碼
一旦 builder 在出現在元件圖中,activity 就可以用它來建立子元件:
public class MyActivity extends Activity {
@Inject ArrayAdapter arrayAdapter;
public void onCreate(Bundle savedInstance) {
// assign singleton instances to fields
// We need to cast to `MyApp` in order to get the right method
MyActivitySubcomponent.Builder builder = (MyActivitySubcomponent.Builder)
((MyApp) getApplication()).getApplicationComponent())
.subcomponentBuilders()
.get(MyActivitySubcomponent.Builder.class)
.get();
builder.activityModule(new MyActivityModule(this)).build().inject(this);
}
}複製程式碼
ProGuard
Dagger 2 應當在沒有 ProGuard 時可以直接使用,但是如果你看到了 library class dagger.producers.monitoring.internal.Monitors$1 extends or implements program class javax.inject.Provider
,你需要確認你的 gradle 配置使用了 annotationProcessor
宣告,而不是 provided
。
常見問題
- 如果你在升級 Dagger 版本(比如從 v2.0 升級到 v 2.5),一些被生成的程式碼會改變。如果你在整合使用舊版本 Dagger 生成的程式碼,你可能會看到
MemberInjector
和actual and former argument lists different in length
錯誤。確保你 clean 過整個專案,並且把所有版本升級到和 Dagger 2 相匹配的版本。
參考資料
- Dagger 2 Github Page
- Sample project using Dagger 2
- Vince Mi's Codepath Meetup Dagger 2 Slides
- code.tutsplus.com/tutorials/d…
- Jake Wharton's Devoxx Dagger 2 Slides
- Jake Wharton's Devoxx Dagger 2 Talk
- Dagger 2 Google Developers Talk
- Dagger 1 to Dagger 2
- Tasting Dagger 2 on Android
- Dagger 2 Testing with Mockito
- Snorkeling with Dagger 2
- Dependency Injection in Java
- Component Dependency vs. Submodules in Dagger 2
- Dagger 2 Component Scopes Test
- Advanced Dagger Talk
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、React、前端、後端、產品、設計 等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃。