用 Dagger 2 實現依賴注入

tanglie1993發表於2017-06-19

用 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 2 實現依賴注入
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 2 實現依賴注入
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 2 實現依賴注入
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 2 實現依賴注入
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 2 實現依賴注入
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 2 實現依賴注入
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前端後端產品設計 等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃

相關文章