在 Android 應用程式中使用 Dagger

Dagger 基本概念頁面說明瞭 Dagger 如何協助您自動化在應用程式中插入依附元件。使用 Dagger 時,您無須編寫繁瑣且容易出錯的樣板程式碼。

最佳做法摘要

  • 盡可能使用 @Inject 的建構函式插入功能,以在 Dagger 圖形中新增類型。如果沒有的話:
    • 使用 @Binds 告知 Dagger 哪些介面應實作。
    • 使用 @Provides 向 Dagger 說明如何提供專案不屬於的類別。
  • 每個元件只能宣告一次模組。
  • 根據使用該註解的生命週期命名範圍註解。範例包括 @ApplicationScope@LoggedUserScope@ActivityScope

新增依附元件

如要在專案中使用 Dagger,請將以下依附元件新增至 build.gradle 檔案中的應用程式。您可以在這個 GitHub 專案中找到最新版本的 Dagger。

Kotlin

plugins {
  id 'kotlin-kapt'
}

dependencies {
    implementation 'com.google.dagger:dagger:2.x'
    kapt 'com.google.dagger:dagger-compiler:2.x'
}

Java

dependencies {
    implementation 'com.google.dagger:dagger:2.x'
    annotationProcessor 'com.google.dagger:dagger-compiler:2.x'
}

Android 中的 Dagger

以 Android 應用程式範例中的依附元件圖表為例,如圖 1。

LoginActivity 依附於 LoginViewModel,取決於 UserRepository,這取決於 UserLocalDataSource 和 UserRemoteDataSource,而這取決於 Retrofit。

圖 1 程式碼範例的依附元件圖表

在 Android 中,您通常會建立一個位於應用程式類別中的 Dagger 圖形,因為您希望應用程式執行時,圖表的執行個體位於記憶體中。這樣一來,圖表就會連結至應用程式生命週期。在某些情況下,您可能也希望在圖表中應用程式結構定義可供使用。因此,您的圖表必須屬於 Application 類別。這種做法的其中一個優點是,其他 Android 架構類別可以使用該圖表。此外,還可以在測試中使用自訂 Application 類別,以簡化測試作業。

由於產生圖表的介面已加上 @Component 註解,因此您可以呼叫 ApplicationComponentApplicationGraph。您通常會將該元件的執行個體保留在自訂 Application 類別中,並在每次需要應用程式圖表時呼叫,如以下程式碼片段所示:

Kotlin

// Definition of the Application graph
@Component
interface ApplicationComponent { ... }

// appComponent lives in the Application class to share its lifecycle
class MyApplication: Application() {
    // Reference to the application graph that is used across the whole app
    val appComponent = DaggerApplicationComponent.create()
}

Java

// Definition of the Application graph
@Component
public interface ApplicationComponent {
}

// appComponent lives in the Application class to share its lifecycle
public class MyApplication extends Application {

    // Reference to the application graph that is used across the whole app
    ApplicationComponent appComponent = DaggerApplicationComponent.create();
}

由於特定 Android 架構類別 (例如活動和片段) 會執行個體化,因此 Dagger 無法為您建立這些類別。針對活動,任何初始化程式碼都必須進入 onCreate() 方法。這表示您不能像在上述範例一樣,在類別的建構函式 (建構函式插入功能) 中使用 @Inject 註解。請改用欄位插入。

您想要 Dagger 為您填入這些依附元件,而非在 onCreate() 方法中建立活動所需的依附元件。如要插入欄位,請改為將 @Inject 註解套用至要從 Dagger 圖表取得的欄位。

Kotlin

class LoginActivity: Activity() {
    // You want Dagger to provide an instance of LoginViewModel from the graph
    @Inject lateinit var loginViewModel: LoginViewModel
}

Java

public class LoginActivity extends Activity {

    // You want Dagger to provide an instance of LoginViewModel from the graph
    @Inject
    LoginViewModel loginViewModel;
}

為簡單起見,LoginViewModel 並非 Android 架構元件 ViewModel;這只是一般做為 ViewModel 的類別。如要進一步瞭解如何插入這些類別,請參閱官方程式碼中的「Android Blueprints Dagger 實作」在「dev-dagger」分支版本中的程式碼。

Dagger 的其中一個考量點是,插入的欄位無法設為不公開。至少須至少具備私人套件的瀏覽權限,如前述程式碼所示。

插入活動

Dagger 必須知道 LoginActivity 必須存取圖表,才能提供所需的 ViewModel。在 Dagger 基本概念頁面,您可以使用 @Component 介面,以透過接觸擁有回傳類型的函式,從圖表取得您想要的物件。在這種情況下,您必須告知 Dagger 物件 (在本範例中為 LoginActivity) 需要插入依附元件。因此,您必須公開一個函式,並將函式視為要求插入物件的參數。

Kotlin

@Component
interface ApplicationComponent {
    // This tells Dagger that LoginActivity requests injection so the graph needs to
    // satisfy all the dependencies of the fields that LoginActivity is requesting.
    fun inject(activity: LoginActivity)
}

Java

@Component
public interface ApplicationComponent {
    // This tells Dagger that LoginActivity requests injection so the graph needs to
    // satisfy all the dependencies of the fields that LoginActivity is injecting.
    void inject(LoginActivity loginActivity);
}

這個函式會通知 Dagger,LoginActivity 要求存取圖形並要求插入。Dagger 必須滿足 LoginActivity 所需的所有依附元件 (LoginViewModel 具有其依附元件)。如果您有多個要求插入的類別,則必須在元件中明確宣告這些類型的確切類型。舉例來說,如果您要求插入 LoginActivityRegistrationActivity,您會有兩個 inject() 方法,而不是一般涵蓋兩種案件的方法。一般的 inject() 方法不會向 Dagger 說明要提供哪些資訊。介面中的函式可以擁有任何名稱,但在接收要做為參數插入的物件時呼叫 inject() 是 Dagger 中的慣例。

如要在活動中插入物件,請使用 Application 類別中定義的 appComponent 並呼叫 inject() 方法,以傳入要求插入的活動的執行個體。

使用活動時,請在呼叫 super.onCreate() 前,在活動的 onCreate() 方法中插入 Dagger,以避免片段還原的問題。在 super.onCreate() 的還原階段,活動會附加可能存取活動繫結的片段。

使用片段時,請在片段的 onAttach() 方法中插入 Dagger。在這種情況下,可以在呼叫 super.onAttach() 之前或之後進行。

Kotlin

class LoginActivity: Activity() {
    // You want Dagger to provide an instance of LoginViewModel from the graph
    @Inject lateinit var loginViewModel: LoginViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        // Make Dagger instantiate @Inject fields in LoginActivity
        (applicationContext as MyApplication).appComponent.inject(this)
        // Now loginViewModel is available

        super.onCreate(savedInstanceState)
    }
}

// @Inject tells Dagger how to create instances of LoginViewModel
class LoginViewModel @Inject constructor(
    private val userRepository: UserRepository
) { ... }

Java

public class LoginActivity extends Activity {

    // You want Dagger to provide an instance of LoginViewModel from the graph
    @Inject
    LoginViewModel loginViewModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // Make Dagger instantiate @Inject fields in LoginActivity
        ((MyApplication) getApplicationContext()).appComponent.inject(this);
        // Now loginViewModel is available

        super.onCreate(savedInstanceState);
    }
}

public class LoginViewModel {

    private final UserRepository userRepository;

    // @Inject tells Dagger how to create instances of LoginViewModel
    @Inject
    public LoginViewModel(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
}

讓我們告訴 Dagger 如何提供其他依附元件以建立圖表:

Kotlin

class UserRepository @Inject constructor(
    private val localDataSource: UserLocalDataSource,
    private val remoteDataSource: UserRemoteDataSource
) { ... }

class UserLocalDataSource @Inject constructor() { ... }
class UserRemoteDataSource @Inject constructor(
    private val loginService: LoginRetrofitService
) { ... }

Java

public class UserRepository {

    private final UserLocalDataSource userLocalDataSource;
    private final UserRemoteDataSource userRemoteDataSource;

    @Inject
    public UserRepository(UserLocalDataSource userLocalDataSource, UserRemoteDataSource userRemoteDataSource) {
        this.userLocalDataSource = userLocalDataSource;
        this.userRemoteDataSource = userRemoteDataSource;
    }
}

public class UserLocalDataSource {

    @Inject
    public UserLocalDataSource() {}
}

public class UserRemoteDataSource {

    private final LoginRetrofitService loginRetrofitService;

    @Inject
    public UserRemoteDataSource(LoginRetrofitService loginRetrofitService) {
        this.loginRetrofitService = loginRetrofitService;
    }
}

Dagger 模組

在這個範例中,您正在使用 Retrofit 網路程式庫。UserRemoteDataSource 具有 LoginRetrofitService 的依附元件。不過,建立 LoginRetrofitService 執行個體的方式與目前為止的不同。這不是類別執行個體化;而是呼叫 Retrofit.Builder() 並傳入不同的參數來設定登入服務的結果。

除了 @Inject 註解以外,您還可以透過其他方式告知 Dagger 如何提供類別的執行個體:Dagger 模組中的資訊。Dagger 模組是一種加上 @Module 註解的類別。在這裡,您可以使用 @Provides 註解定義依附元件。

Kotlin

// @Module informs Dagger that this class is a Dagger Module
@Module
class NetworkModule {

    // @Provides tell Dagger how to create instances of the type that this function
    // returns (i.e. LoginRetrofitService).
    // Function parameters are the dependencies of this type.
    @Provides
    fun provideLoginRetrofitService(): LoginRetrofitService {
        // Whenever Dagger needs to provide an instance of type LoginRetrofitService,
        // this code (the one inside the @Provides method) is run.
        return Retrofit.Builder()
                .baseUrl("https://example.com")
                .build()
                .create(LoginService::class.java)
    }
}

Java

// @Module informs Dagger that this class is a Dagger Module
@Module
public class NetworkModule {

    // @Provides tell Dagger how to create instances of the type that this function
    // returns (i.e. LoginRetrofitService).
    // Function parameters are the dependencies of this type.
    @Provides
    public LoginRetrofitService provideLoginRetrofitService() {
        // Whenever Dagger needs to provide an instance of type LoginRetrofitService,
        // this code (the one inside the @Provides method) is run.
        return new Retrofit.Builder()
                .baseUrl("https://example.com")
                .build()
                .create(LoginService.class);
    }
}

模組可讓您以語意方式封裝如何提供物件的資訊。如您所見,您已呼叫類別 NetworkModule,以組成提供網路相關物件的邏輯。如果應用程式擴展,您也可以新增如何在此提供 OkHttpClient 或如何設定 Gson 或是 Moshi

@Provides 方法的依附元件是該方法的參數。針對上一個方法,LoginRetrofitService 無需依附元件,即可提供,因為該方法沒有任何參數。如果您已宣告 OkHttpClient 做為參數,Dagger 就必須提供圖表中的 OkHttpClient 執行個體,以滿足 LoginRetrofitService 的依附元件。例如:

Kotlin

@Module
class NetworkModule {
    // Hypothetical dependency on LoginRetrofitService
    @Provides
    fun provideLoginRetrofitService(
        okHttpClient: OkHttpClient
    ): LoginRetrofitService { ... }
}

Java

@Module
public class NetworkModule {

    @Provides
    public LoginRetrofitService provideLoginRetrofitService(OkHttpClient okHttpClient) {
        ...
    }
}

為了讓 Dagger 圖表瞭解這個模組,您必須將其新增至 @Component 介面,如下所示:

Kotlin

// The "modules" attribute in the @Component annotation tells Dagger what Modules
// to include when building the graph
@Component(modules = [NetworkModule::class])
interface ApplicationComponent {
    ...
}

Java

// The "modules" attribute in the @Component annotation tells Dagger what Modules
// to include when building the graph
@Component(modules = NetworkModule.class)
public interface ApplicationComponent {
    ...
}

如要將類型新增至 Dagger 圖表,建議您使用建構函式插入功能 (即在類別建構函式上使用 @Inject 註解)。有時無法做到,因此您需要使用 Dagger 模組。例如,當 Dagger 使用計算結果來判斷如何建立物件執行個體時,就是其中一個例子。每當需要提供該類型的執行個體時,Dagger 都會在 @Provides 方法中執行程式碼。

範例中的 Dagger 圖形現如下所示:

LoginActivity 依附元件圖的圖表

圖 2. 表示 Dagger 插入 LoginActivity 的圖表

圖表的進入點為 LoginActivity。由於 LoginActivity 插入了 LoginViewModel,所以 Dagger 建構的圖表會知道如何提供 LoginViewModel 的執行個體,並遞迴地提供其依附元件。由於類別建構函式上的 @Inject 註解,Dagger 知道如何執行這項操作。

在 Dagger 產生的 ApplicationComponent 中,有一個工廠類型的方法,可取得其已知如何提供的所有類別的執行個體。在此範例中,Dagger 委派至 ApplicationComponent 中包含的 NetworkModule,以取得 LoginRetrofitService 的執行個體。

Dagger 範圍

已在Dagger 基本概念頁面中提及範圍,以便在元件中擁有特定類型的專屬執行個體。也就是「限定元件的生命週期的類型」

由於您可能會想在應用程式的其他功能中使用 UserRepository,且可能不會在每次需要時建立新物件,因此可以將其指定為整個應用程式的專屬執行個體。這是與 LoginRetrofitService 相同的執行個體:建立費用可能相當昂貴,而且還需要該物件的專屬執行個體可以重複使用。建立 UserRemoteDataSource 執行個體並不昂貴,因此,限定元件的生命週期的類型並不必要。

@Singletonjavax.inject 套件隨附的唯一範圍註解。您可以用此來註解 ApplicationComponent,以及要在整個應用程式中重複使用的物件。

Kotlin

@Singleton
@Component(modules = [NetworkModule::class])
interface ApplicationComponent {
    fun inject(activity: LoginActivity)
}

@Singleton
class UserRepository @Inject constructor(
    private val localDataSource: UserLocalDataSource,
    private val remoteDataSource: UserRemoteDataSource
) { ... }

@Module
class NetworkModule {
    // Way to scope types inside a Dagger Module
    @Singleton
    @Provides
    fun provideLoginRetrofitService(): LoginRetrofitService { ... }
}

Java

@Singleton
@Component(modules = NetworkModule.class)
public interface ApplicationComponent {
    void inject(LoginActivity loginActivity);
}

@Singleton
public class UserRepository {

    private final UserLocalDataSource userLocalDataSource;
    private final UserRemoteDataSource userRemoteDataSource;

    @Inject
    public UserRepository(UserLocalDataSource userLocalDataSource, UserRemoteDataSource userRemoteDataSource) {
        this.userLocalDataSource = userLocalDataSource;
        this.userRemoteDataSource = userRemoteDataSource;
    }
}

@Module
public class NetworkModule {

    @Singleton
    @Provides
    public LoginRetrofitService provideLoginRetrofitService() { ... }
}

將範圍套用至物件時,請小心不要導入記憶體流失情形。只要範圍元件位於記憶體中,已建立的物件就會位於記憶體中。由於 ApplicationComponent 會在應用程式啟動時建立 (在 Application 類別中),因此會在應用程式遭到刪除時刪除。因此,UserRepository 的專屬執行個體一律會保留在記憶體中,直到應用程式刪除為止。

Dagger 子元件

如果您的登入流程 (由單一 LoginActivity 管理) 包含多個片段,您應在所有片段中重複使用相同的 LoginViewModel 執行個體。@Singleton 無法為 LoginViewModel 加上註解,以重複使用執行個體,原因如下:

  1. 完成流程後,LoginViewModel 的執行個體會保留在記憶體中。

  2. 希望每個登入流程使用不同的 LoginViewModel 執行個體。例如,如果使用者登出,您會想要不同的 LoginViewModel 執行個體,而不是與使用者首次登入時相同的執行個體。

如要將 LoginViewModel 的範圍限制為 LoginActivity 的生命週期,您必須針對登入流程和新的範圍建立新的元件 (新的子圖表)。

首先,請建立登入流程專用的圖表。

Kotlin

@Component
interface LoginComponent {}

Java

@Component
public interface LoginComponent {
}

現在 LoginActivity 應從 LoginComponent 插入,因為其有登入專屬設定。這樣即可避免從 ApplicationComponent 類別插入 LoginActivity

Kotlin

@Component
interface LoginComponent {
    fun inject(activity: LoginActivity)
}

Java

@Component
public interface LoginComponent {
    void inject(LoginActivity loginActivity);
}

LoginComponent 必須能從 ApplicationComponent 存取物件,因為 LoginViewModel 依附於 UserRepository。告知 Dagger 您希望新元件使用另一個元件的部分,就是使用「Dagger 子元件」。新元件必須是包含共用資源的元件的子元件。

「子元件」是沿用和擴充父項元件物件圖表的元件。因此,在父項元件中提供的所有物件也會在子元件中提供。這樣一來,子元件中的物件就能依賴於父項元件提供的物件。

如要建立子元件的執行個體,您需要父項元件的執行個體。因此,由父項元件提供給子元件的物件仍會限定在父項元件的範圍。

在範例中,您必須將 LoginComponent 定義為 ApplicationComponent 的子元件。方法是以 @Subcomponent 註解 LoginComponent

Kotlin

// @Subcomponent annotation informs Dagger this interface is a Dagger Subcomponent
@Subcomponent
interface LoginComponent {

    // This tells Dagger that LoginActivity requests injection from LoginComponent
    // so that this subcomponent graph needs to satisfy all the dependencies of the
    // fields that LoginActivity is injecting
    fun inject(loginActivity: LoginActivity)
}

Java

// @Subcomponent annotation informs Dagger this interface is a Dagger Subcomponent
@Subcomponent
public interface LoginComponent {

    // This tells Dagger that LoginActivity requests injection from LoginComponent
    // so that this subcomponent graph needs to satisfy all the dependencies of the
    // fields that LoginActivity is injecting
    void inject(LoginActivity loginActivity);
}

您還必須在 LoginComponent 內定義子元件工廠,讓 ApplicationComponent 瞭解如何建立 LoginComponent 的執行個體。

Kotlin

@Subcomponent
interface LoginComponent {

    // Factory that is used to create instances of this subcomponent
    @Subcomponent.Factory
    interface Factory {
        fun create(): LoginComponent
    }

    fun inject(loginActivity: LoginActivity)
}

Java

@Subcomponent
public interface LoginComponent {

    // Factory that is used to create instances of this subcomponent
    @Subcomponent.Factory
    interface Factory {
        LoginComponent create();
    }

    void inject(LoginActivity loginActivity);
}

如要告知 Dagger LoginComponentApplicationComponent 的子元件,您必須透過下列方式指示:

  1. 建立新的 Dagger 模組 (例如 SubcomponentsModule),用來將子元件的類別傳遞至註解的 subcomponents 屬性。

    Kotlin

    // The "subcomponents" attribute in the @Module annotation tells Dagger what
    // Subcomponents are children of the Component this module is included in.
    @Module(subcomponents = LoginComponent::class)
    class SubcomponentsModule {}
    

    Java

    // The "subcomponents" attribute in the @Module annotation tells Dagger what
    // Subcomponents are children of the Component this module is included in.
    @Module(subcomponents = LoginComponent.class)
    public class SubcomponentsModule {
    }
    
  2. 將新模組 (例如 SubcomponentsModule) 新增至 ApplicationComponent

    Kotlin

    // Including SubcomponentsModule, tell ApplicationComponent that
    // LoginComponent is its subcomponent.
    @Singleton
    @Component(modules = [NetworkModule::class, SubcomponentsModule::class])
    interface ApplicationComponent {
    }
    

    Java

    // Including SubcomponentsModule, tell ApplicationComponent that
    // LoginComponent is its subcomponent.
    @Singleton
    @Component(modules = {NetworkModule.class, SubcomponentsModule.class})
    public interface ApplicationComponent {
    }
    

    請注意,ApplicationComponent 不需要插入 LoginActivity,因為該責任現屬於 LoginComponent,因此您可從 ApplicationComponent 移除 inject() 方法。

    ApplicationComponent 的消費者需要知道如何建立 LoginComponent 的執行個體。父項元件必須在其介面中新增方法,讓客戶能夠從父項元件的執行個體中建立子元件的執行個體:

  3. 公開在介面中建立 LoginComponent 執行個體的工廠:

    Kotlin

    @Singleton
    @Component(modules = [NetworkModule::class, SubcomponentsModule::class])
    interface ApplicationComponent {
    // This function exposes the LoginComponent Factory out of the graph so consumers
    // can use it to obtain new instances of LoginComponent
    fun loginComponent(): LoginComponent.Factory
    }
    

    Java

    @Singleton
    @Component(modules = { NetworkModule.class, SubcomponentsModule.class} )
    public interface ApplicationComponent {
    // This function exposes the LoginComponent Factory out of the graph so consumers
    // can use it to obtain new instances of LoginComponent
    LoginComponent.Factory loginComponent();
    }
    

將範圍指派給子元件

當您建構專案時,可以建立 ApplicationComponentLoginComponent 的執行個體。因您希望只要應用程式在記憶體中,就使用相同的圖表的執行個體,因此將 ApplicationComponent 附加至應用程式的生命週期。

LoginComponent 的生命週期為何?需要使用 LoginComponent 的其中一個原因是,您需要在登入相關片段之間共用相同的 LoginViewModel 執行個體。此外,每當有新的登入流程時,您希望不同的 LoginViewModel 執行個體。LoginActivityLoginComponent 的正確生命週期:針對每個新活動,您需要新的 LoginComponent 執行個體,以及可使用該 LoginComponent 執行個體的片段。

因為 LoginComponent 附加至 LoginActivity 生命週期,您必須在活動中保留對元件的參照,秉持相同於保留參照在 Application 中的 applicationComponent 類別如此一來,片段就可以存取。

Kotlin

class LoginActivity: Activity() {
    // Reference to the Login graph
    lateinit var loginComponent: LoginComponent
    ...
}

Java

public class LoginActivity extends Activity {

    // Reference to the Login graph
    LoginComponent loginComponent;

    ...
}

請注意,您不應使用 @Inject 註解變數 loginComponent,因為您不希望 Dagger 提供該變數。

您可以使用 ApplicationComponent 取得 LoginComponent 的參照,然後插入 LoginActivity,如下所示:

Kotlin

class LoginActivity: Activity() {
    // Reference to the Login graph
    lateinit var loginComponent: LoginComponent

    // Fields that need to be injected by the login graph
    @Inject lateinit var loginViewModel: LoginViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        // Creation of the login graph using the application graph
        loginComponent = (applicationContext as MyDaggerApplication)
                            .appComponent.loginComponent().create()

        // Make Dagger instantiate @Inject fields in LoginActivity
        loginComponent.inject(this)

        // Now loginViewModel is available

        super.onCreate(savedInstanceState)
    }
}

Java

public class LoginActivity extends Activity {

    // Reference to the Login graph
    LoginComponent loginComponent;

    // Fields that need to be injected by the login graph
    @Inject
    LoginViewModel loginViewModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // Creation of the login graph using the application graph
        loginComponent = ((MyApplication) getApplicationContext())
                                .appComponent.loginComponent().create();

        // Make Dagger instantiate @Inject fields in LoginActivity
        loginComponent.inject(this);

        // Now loginViewModel is available

        super.onCreate(savedInstanceState);
    }
}

LoginComponent 是在活動的 onCreate() 方法中建立,系統會在活動刪除時隱含刪除。

每次要求時,LoginComponent 都必須提供相同的 LoginViewModel 執行個體。您可以建立自訂註解範圍,並為其加上 LoginComponentLoginViewModel 註解,藉此確保這一點。請注意,由於 @Singleton 註解已用於父項元件,因此會將物件設為應用程式單例模式 (整個應用程式的不重複執行個體),因此無法使用。您必須建立其他註解範圍。

在此情況下,您可以命名此範圍為 @LoginScope,但我們不建議這麼做。範圍註解的名稱不應明確符合其用途。請改為根據其生命週期命名,因為 RegistrationComponentSettingsComponent 等同層級元件可以重複使用註解。因此建議您命名其為 @ActivityScope 而非 @LoginScope

Kotlin

// Definition of a custom scope called ActivityScope
@Scope
@Retention(value = AnnotationRetention.RUNTIME)
annotation class ActivityScope

// Classes annotated with @ActivityScope are scoped to the graph and the same
// instance of that type is provided every time the type is requested.
@ActivityScope
@Subcomponent
interface LoginComponent { ... }

// A unique instance of LoginViewModel is provided in Components
// annotated with @ActivityScope
@ActivityScope
class LoginViewModel @Inject constructor(
    private val userRepository: UserRepository
) { ... }

Java

// Definition of a custom scope called ActivityScope
@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface ActivityScope {}

// Classes annotated with @ActivityScope are scoped to the graph and the same
// instance of that type is provided every time the type is requested.
@ActivityScope
@Subcomponent
public interface LoginComponent { ... }

// A unique instance of LoginViewModel is provided in Components
// annotated with @ActivityScope
@ActivityScope
public class LoginViewModel {

    private final UserRepository userRepository;

    @Inject
    public LoginViewModel(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
}

現在,如果您有兩個片段需要 LoginViewModel,這兩個片段都會有相同的執行個體。舉例來說,如果您有 LoginUsernameFragmentLoginPasswordFragment,則 LoginComponent 必須插入這些函式:

Kotlin

@ActivityScope
@Subcomponent
interface LoginComponent {

    @Subcomponent.Factory
    interface Factory {
        fun create(): LoginComponent
    }

    // All LoginActivity, LoginUsernameFragment and LoginPasswordFragment
    // request injection from LoginComponent. The graph needs to satisfy
    // all the dependencies of the fields those classes are injecting
    fun inject(loginActivity: LoginActivity)
    fun inject(usernameFragment: LoginUsernameFragment)
    fun inject(passwordFragment: LoginPasswordFragment)
}

Java

@ActivityScope
@Subcomponent
public interface LoginComponent {

    @Subcomponent.Factory
    interface Factory {
        LoginComponent create();
    }

    // All LoginActivity, LoginUsernameFragment and LoginPasswordFragment
    // request injection from LoginComponent. The graph needs to satisfy
    // all the dependencies of the fields those classes are injecting
    void inject(LoginActivity loginActivity);
    void inject(LoginUsernameFragment loginUsernameFragment);
    void inject(LoginPasswordFragment loginPasswordFragment);
}

元件會存取位於 LoginActivity 物件中的元件執行個體。LoginUserNameFragment 程式碼範例如以下程式碼片段所示:

Kotlin

class LoginUsernameFragment: Fragment() {

    // Fields that need to be injected by the login graph
    @Inject lateinit var loginViewModel: LoginViewModel

    override fun onAttach(context: Context) {
        super.onAttach(context)

        // Obtaining the login graph from LoginActivity and instantiate
        // the @Inject fields with objects from the graph
        (activity as LoginActivity).loginComponent.inject(this)

        // Now you can access loginViewModel here and onCreateView too
        // (shared instance with the Activity and the other Fragment)
    }
}

Java

public class LoginUsernameFragment extends Fragment {

    // Fields that need to be injected by the login graph
    @Inject
    LoginViewModel loginViewModel;

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);

        // Obtaining the login graph from LoginActivity and instantiate
        // the @Inject fields with objects from the graph
        ((LoginActivity) getActivity()).loginComponent.inject(this);

        // Now you can access loginViewModel here and onCreateView too
        // (shared instance with the Activity and the other Fragment)
    }
}

LoginPasswordFragment 也是一樣:

Kotlin

class LoginPasswordFragment: Fragment() {

    // Fields that need to be injected by the login graph
    @Inject lateinit var loginViewModel: LoginViewModel

    override fun onAttach(context: Context) {
        super.onAttach(context)

        (activity as LoginActivity).loginComponent.inject(this)

        // Now you can access loginViewModel here and onCreateView too
        // (shared instance with the Activity and the other Fragment)
    }
}

Java

public class LoginPasswordFragment extends Fragment {

    // Fields that need to be injected by the login graph
    @Inject
    LoginViewModel loginViewModel;

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);

        ((LoginActivity) getActivity()).loginComponent.inject(this);

        // Now you can access loginViewModel here and onCreateView too
        // (shared instance with the Activity and the other Fragment)
    }
}

圖 3 顯示了新子元件中的 Dagger 圖表外觀。標有白點的類別 (UserRepositoryLoginRetrofitServiceLoginViewModel) 是各自元件中具有不重複執行個體範圍的類別。

新增最後一個子元件後的應用程式圖表

圖 3. 呈現您為 Android 應用程式範例建立的圖表

讓我們來解析圖表的各個部分:

  1. ApplicationComponent 中已納入 NetworkModule (因此為 LoginRetrofitService),因為您已在元件中指定。

  2. UserRepository 保留在 ApplicationComponent 中,因為其範圍為 ApplicationComponent。如果專案成長,您希望在不同功能 (例如註冊) 之間共用相同的執行個體。

    因為 UserRepositoryApplicationComponent 的一部分,其依附元件 (例如 UserLocalDataSourceUserRemoteDataSource) 也必須加入這個元件,才能提供 UserRepository 的執行個體。

  3. LoginComponent 中包含 LoginViewModel,因為 LoginComponent 插入的類別才需使用。LoginViewModel 未包含在 ApplicationComponent,因為 ApplicationComponent 中沒有任何依附元件需要 LoginViewModel

    同樣地,如果您尚未將 UserRepository 範圍設為 ApplicationComponent,Dagger 會自動包含 UserRepository 及其依附元件做為 LoginComponent 的一部分,因為這是目前唯一使用 UserRepository 的地方。

除了將物件範圍限定為不同的生命週期,建立子元件也是封裝應用程式不同部分的最佳做法

建構應用程式以根據應用程式流程建立不同的 Dagger 子圖表,有助於在記憶體和啟動時間方面取得更高效能且可擴充的應用程式

建構 Dagger 圖表的最佳做法

為應用程式建構 Dagger 圖時:

  • 建立元件時,應考量影響元件生命週期的元素為何。在這種情況下,Application 類別負責 ApplicationComponentLoginActivity 則負責 LoginComponent

  • 請僅在適當情況下使用限定範圍。過度限定範圍可能會對應用程式執行階段效能造成負面影響:只要元件位於記憶體中,並取得範圍內較昂貴的物件,記憶體就會留存在記憶體中。 Dagger 提供物件時,將使用 DoubleCheck 鎖定,而非工廠類型提供者。

測試使用 Dagger 的專案

使用依附元件插入架構 (例如 Dagger) 的好處之一是可讓您更輕鬆地測試程式碼。

單元測試

您不必使用 Dagger 進行「單元測試」。測試使用建構函式插入的類別時,您不需要使用 Dagger 對該類別執行個體化。您可以直接呼叫其建構函式直接傳遞假的或模擬的依附元件,方法與未加註註解時一樣。

例如,在測試 LoginViewModel 時:

Kotlin

@ActivityScope
class LoginViewModel @Inject constructor(
    private val userRepository: UserRepository
) { ... }

class LoginViewModelTest {

    @Test
    fun `Happy path`() {
        // You don't need Dagger to create an instance of LoginViewModel
        // You can pass a fake or mock UserRepository
        val viewModel = LoginViewModel(fakeUserRepository)
        assertEquals(...)
    }
}

Java

@ActivityScope
public class LoginViewModel {

    private final UserRepository userRepository;

    @Inject
    public LoginViewModel(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
}

public class LoginViewModelTest {

    @Test
    public void happyPath() {
        // You don't need Dagger to create an instance of LoginViewModel
        // You can pass a fake or mock UserRepository
        LoginViewModel viewModel = new LoginViewModel(fakeUserRepository);
        assertEquals(...);
    }
}

端對端測試

針對「整合測試」,最佳做法是建立用於測試的 TestApplicationComponent正式版和測試使用其他元件設定

在應用程式中,這要求更多預先模組的設計。測試元件會擴充正式版元件,並安裝不同的模組組合。

Kotlin

// TestApplicationComponent extends from ApplicationComponent to have them both
// with the same interface methods. You need to include the modules of the
// component here as well, and you can replace the ones you want to override.
// This sample uses FakeNetworkModule instead of NetworkModule
@Singleton
@Component(modules = [FakeNetworkModule::class, SubcomponentsModule::class])
interface TestApplicationComponent : ApplicationComponent {
}

Java

// TestApplicationComponent extends from ApplicationComponent to have them both
// with the same interface methods. You need to include the modules of the
// Component here as well, and you can replace the ones you want to override.
// This sample uses FakeNetworkModule instead of NetworkModule
@Singleton
@Component(modules = {FakeNetworkModule.class, SubcomponentsModule.class})
public interface TestApplicationComponent extends ApplicationComponent {
}

FakeNetworkModuleNetworkModule 原始版本的假實作。您可以在其中提供要替換的假執行個體或模擬圖。

Kotlin

// In the FakeNetworkModule, pass a fake implementation of LoginRetrofitService
// that you can use in your tests.
@Module
class FakeNetworkModule {
    @Provides
    fun provideLoginRetrofitService(): LoginRetrofitService {
        return FakeLoginService()
    }
}

Java

// In the FakeNetworkModule, pass a fake implementation of LoginRetrofitService
// that you can use in your tests.
@Module
public class FakeNetworkModule {

    @Provides
    public LoginRetrofitService provideLoginRetrofitService() {
        return new FakeLoginService();
    }
}

在整合或端對端測試中,您將使用建立 TestApplicationComponentTestApplication 而非 ApplicationComponent

Kotlin

// Your test application needs an instance of the test graph
class MyTestApplication: MyApplication() {
    override val appComponent = DaggerTestApplicationComponent.create()
}

Java

// Your test application needs an instance of the test graph
public class MyTestApplication extends MyApplication {
    ApplicationComponent appComponent = DaggerTestApplicationComponent.create();
}

接著,將這個測試應用程式用於您將用來執行檢測設備測試的自訂 TestRunner。如要進一步瞭解相關資訊,請參閱「在 Android 應用程式程式碼研究室中使用 Dagger」。

使用 Dagger 模組

可透過 Dagger 模組封裝如何以語意方式提供物件。您可以在元件中加入模組,但您也可以將模組納入其他模組。這項功能很強大,但很容易濫用。

將模組新增至元件或其他模組後,即表示該模組已在 Dagger 圖表中。Dagger 可在元件中提供這些物件。在新增模組之前,請檢查該模組是否已加入元件中,或是透過編譯專案,看看 Dagger 能否找到該模組所需的依附元件,藉此確認是否已加入 Dagger 圖表。

良好的做法指定元件中,模組只應宣告一次 (特定進階 Dagger 用途除外)。

假設您以這種方式設定圖表。ApplicationComponent 包含 Module1Module2,而 Module1 包含 ModuleX

Kotlin

@Component(modules = [Module1::class, Module2::class])
interface ApplicationComponent { ... }

@Module(includes = [ModuleX::class])
class Module1 { ... }

@Module
class Module2 { ... }

Java

@Component(modules = {Module1.class, Module2.class})
public interface ApplicationComponent { ... }

@Module(includes = {ModuleX.class})
public class Module1 { ... }

@Module
public class Module2 { ... }

現在,Module2 取決於 ModuleX 提供的類別。不良做法Module2 中包含 ModuleX,因為在圖中,ModuleX 已包含了兩次,如以下程式碼片段所示:

Kotlin

// Bad practice: ModuleX is declared multiple times in this Dagger graph
@Component(modules = [Module1::class, Module2::class])
interface ApplicationComponent { ... }

@Module(includes = [ModuleX::class])
class Module1 { ... }

@Module(includes = [ModuleX::class])
class Module2 { ... }

Java

// Bad practice: ModuleX is declared multiple times in this Dagger graph.
@Component(modules = {Module1.class, Module2.class})
public interface ApplicationComponent { ... }

@Module(includes = ModuleX.class)
public class Module1 { ... }

@Module(includes = ModuleX.class)
public class Module2 { ... }

建議改用下列方法:

  1. 重構模組,並將通用模組解壓縮至元件。
  2. 建立含有模組共用物件的新模組,並將其擷取至元件。

不以這種方式重構會導致許多模組將彼此包含在內,而沒有清楚的組織架構,因此會難以查看各個依附元件的來源。

良好做法 (選項 1):Dagger 圖表會宣告 ModuleX 一次。

Kotlin

@Component(modules = [Module1::class, Module2::class, ModuleX::class])
interface ApplicationComponent { ... }

@Module
class Module1 { ... }

@Module
class Module2 { ... }

Java

@Component(modules = {Module1.class, Module2.class, ModuleX.class})
public interface ApplicationComponent { ... }

@Module
public class Module1 { ... }

@Module
public class Module2 { ... }

良好做法 (選項 2):來自 ModuleXModule1Module2 常見的依附元件,解壓縮到在元件中名為 ModuleXCommon 的新模組。然後透過每個模組專屬的依附元件來建立這兩個名為 ModuleXWithModule1DependenciesModuleXWithModule2Dependencies 的其他模組。在 Dagger 圖表中,所有模組都會宣告一次。

Kotlin

@Component(modules = [Module1::class, Module2::class, ModuleXCommon::class])
interface ApplicationComponent { ... }

@Module
class ModuleXCommon { ... }

@Module
class ModuleXWithModule1SpecificDependencies { ... }

@Module
class ModuleXWithModule2SpecificDependencies { ... }

@Module(includes = [ModuleXWithModule1SpecificDependencies::class])
class Module1 { ... }

@Module(includes = [ModuleXWithModule2SpecificDependencies::class])
class Module2 { ... }

Java

@Component(modules = {Module1.class, Module2.class, ModuleXCommon.class})
public interface ApplicationComponent { ... }

@Module
public class ModuleXCommon { ... }

@Module
public class ModuleXWithModule1SpecificDependencies { ... }

@Module
public class ModuleXWithModule2SpecificDependencies { ... }

@Module(includes = ModuleXWithModule1SpecificDependencies.class)
public class Module1 { ... }

@Module(includes = ModuleXWithModule2SpecificDependencies.class)
public class Module2 { ... }

輔助植入

輔助插入是一種 DI 模式,用於建構物件,其中部分參數可由 DI 架構提供,而其他參數必須在使用者建立時傳入。

在 Android 中,這個模式在「詳細資料」畫面中很常見,因為系統要顯示的元素 ID 在執行階段才知道,而非在編譯器產生 DI 圖形時的編譯時間。如要進一步瞭解如何透過 Dagger 輔助植入,請參閱「Dagger 說明文件」。

結語

如果您尚未讀過,請參閱「最佳做法部分」。如要瞭解如何在 Android 應用程式中使用 Dagger,請參閱「在 Android 應用程式程式碼研究室中使用 Dagger」。