用 Dagger 2 實現依賴注入

用 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 中聲明瞭 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 遍歷依賴關係圖,並且生成易於理解和追蹤的代碼。而且,它可以節約大量的樣板代碼,使你不再需要手寫,手動獲取引用並把它們傳遞給其他對象作爲依賴。它也簡化了重構,因爲你可以聚焦於構建模塊本身,而不是它們被創建的順序。

  • 更簡單的單元和集成測試 因爲依賴圖是爲我們創建的,我們可以輕易換出用於創建網絡響應的模塊,並模擬這種行爲。

  • 實例範圍 你不僅可以輕易地管理持續整個應用生命週期的實例,也可以利用 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 注入概要

最簡單的例子是用 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 相匹配的版本。

參考資料

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章