用 Dagger 2 實現依賴注入

用 Dagger 2 實現依賴注入

概要

不少 Android 應用依賴於一些含有其它依賴的對象。例如,一個 Twitter API 客戶端可能須要經過 Retrofit 之類的網絡庫來構建。要使用這個庫,你可能還須要添加 Gson 這樣的解析庫。另外,實現認證或緩存的庫可能須要使用 shared preferences 或其它通用存儲方式。這就須要先把它們實例化,並建立一個隱含的依賴鏈。html

若是你不熟悉依賴注入,看看這個短視頻。前端

Dagger 2 爲你解析這些依賴,並生成把它們綁定在一塊兒的代碼。也有不少其它的 Java 依賴注入框架,但它們中大多數是有缺陷的,好比依賴 XML,須要在運行時驗證依賴,或者在起始時形成性能負擔。 Dagger 2 純粹依賴於 Java 註解解析器以及編譯時檢查來分析並驗證依賴。它被認爲是目前最高效的依賴注入框架之一。java

優勢

這是使用 Dagger 2 的一系列其它優點:react

  • 簡化共享實例訪問。就像 ButterKnife 庫簡化了引用View, event handler 和 resources 的方式同樣,Dagger 2 提供了一個簡單的方式獲取對共享對象的引用。例如,一旦咱們在 Dagger 中聲明瞭 MyTwitterApiClientSharedPreferences 的單例,就能夠用一個簡單的 @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 遍歷依賴關係圖,而且生成易於理解和追蹤的代碼。並且,它能夠節約大量的樣板代碼,使你再也不須要手寫,手動獲取引用並把它們傳遞給其餘對象做爲依賴。它也簡化了重構,由於你能夠聚焦於構建模塊自己,而不是它們被建立的順序。android

  • 更簡單的單元和集成測試 由於依賴圖是爲咱們建立的,咱們能夠輕易換出用於建立網絡響應的模塊,並模擬這種行爲。ios

  • 實例範圍 你不只能夠輕易地管理持續整個應用生命週期的實例,也能夠利用 Dagger 2 來定義生命週期更短(好比和一個用戶 session 或 Activity 生命週期相綁定)的實例。 git

設置

默認的 Android Studio 不把生成的 Dagger 2 代碼視做合法的類,由於它們一般並不被加入 source 路徑。但引入 android-apt 插件後,它會把這些文件加入 IDE classpath,從而提供更好的可見性。github

確保升級 到最新的 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 中,而只是把它們用於處理註解。這能夠避免不當心引用它們。api

建立單例

Dagger 注入概要

最簡單的例子是用 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 實例 須要依賴 GsonOkHttpClient

定義注入目標

Dagger 使你的 activity, fragment, 或 service 中的域能夠經過 @Inject 註解和調用 inject() 方法被賦值。調用 inject() 將會使得 Dagger 2 在依賴圖中尋找合適類型的單例。若是找到了一個,它就把引用賦值給對應的域。例如,在下面的例子中,它會嘗試找到一個返回MyTwitterApiClientSharedPreferences 類型的 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);
    }複製代碼

限定詞類型

Dagger Qualifiers

若是咱們須要同一類型的兩個不一樣對象,咱們可使用 @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 做用域

在 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 討論 上有更多的細節。

依賴組件

Dagger 組件依賴

若是你想要建立一個組件,使它的生命週期和已登陸用戶的 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();複製代碼

示例代碼 中有一個實際的例子。

子組件

Dagger 子組件

使用子組件是擴展組件對象圖的另外一種方式。就像帶有依賴的組件同樣,子組件有本身的的生命週期,並且在全部對子組件的引用都失效以後,能夠被垃圾回收。此外它們做用域的限制也同樣。使用這個方式的一個優勢是你不須要定義全部的下游組件。

另外一個主要的不一樣是,子組件須要在父組件中聲明。

這是爲一個 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 版本起可用

Dagger 子組件 builder

子組件 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 生成的代碼,你可能會看到 MemberInjectoractual and former argument lists different in length 錯誤。確保你 clean 過整個項目,而且把全部版本升級到和 Dagger 2 相匹配的版本。

參考資料


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOSReact前端後端產品設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃

相關文章
相關標籤/搜索