在多模組應用程式中使用 Dagger

含有多個 Gradle 模組的專案稱為多模組專案。如果是以單一 APK 形式 (不含任何功能模組) 的多模組,通常為可依附於專案大部分模組的 app 模組及其餘模組經常依附的 basecoreapp 模組通常包含您的 Application 類別,而 base 模組則包含專案中所有模組共用的所有通用類別。

您可以使用 app 模組宣告應用程式元件 (例如下圖中的 ApplicationComponent),藉此提供其他元件可能需要的物件和應用程式的單例模式。舉例來說,OkHttpClient 模組中定義的 ApplicationComponent 會提供 OkHttpClient 等類別、JSON 剖析器、資料庫的存取子或可能定義的 SharedPreferences 物件。appcore

app 模組中,您還可以有壽命較短的其他元件。例如,在登入後,具有使用者專屬設定的 UserComponent (例如 UserSession)。

在專案的不同模組中,您可以定義至少一項具有該模組特定邏輯的子元件,如圖 1 所示。

圖 1 多模組專案中的 Dagger 圖表範例

舉例來說,在 login 模組中,您可以將 LoginComponent 範圍指定為自訂 @ModuleScope 註解,以提供該功能通用的物件 (例如 LoginRepository)。在該模組中,您也可以有其他依附於 LoginComponent 且包含不同的自訂範圍的元件,舉例來說 @FeatureScope (適用於 LoginActivityComponent) 或 TermsAndConditionsComponent (可以在這裡設定更多特定功能的邏輯的範圍,例如 ViewModel 物件)。

至於其他模組 (例如 Registration),則需要進行類似的設定。

多模組專案的一般規則是,相同層級的模組不應該彼此相依。如果可以,請考慮該共用邏輯 (兩者之間的依附元件) 是否應屬於父項模組。如果是這種情況,重構以將類別移至父項模組;否則,請建立擴充父項模組的新模組,並同時讓兩個原始模組擴充新的模組。

最佳做法為在以下例子的模組中建立元件:

  • 您必須執行欄位插入,就像 LoginActivityComponent 一樣。

  • 請務必設定物件範圍,與 LoginComponent 相同。

如果上述兩者均不適用,您需要告知 Dagger 如何提供該模組的物件、建立和顯示 Dagger 模組 @Provides@Binds 方法 (如果建構插入功能不適用於這些類別)。

Dagger 子元件實作

在 Android 應用程式中使用 Dagger」文件頁面說明如何建立及使用子元件。不過,您無法使用相同的程式碼,因為功能模組並不知道 app 模組。舉例來說,如果您考慮一般的登入流程和我們在前一頁中提到的程式碼,就不會再編譯:

Kotlin

class LoginActivity: Activity() {
  ...

  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)
    ...
  }
}

Java

public class LoginActivity extends Activity {
    ...

    @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);

        ...
    }
}

原因在於 login 模組不知道 MyApplicationappComponent。為了讓功能順利運作,您必須在功能模組中定義介面,並提供 MyApplication 需要實作的 FeatureComponent

如以下範例中,您可以定義 LoginComponentProvider 介面,為登入流程的 login 模組中提供 LoginComponent

Kotlin

interface LoginComponentProvider {
    fun provideLoginComponent(): LoginComponent
}

Java

public interface LoginComponentProvider {
   public LoginComponent provideLoginComponent();
}

現在,LoginActivity 會使用該介面,而非上述定義的程式碼片段:

Kotlin

class LoginActivity: Activity() {
  ...

  override fun onCreate(savedInstanceState: Bundle?) {
    loginComponent = (applicationContext as LoginComponentProvider)
                        .provideLoginComponent()

    loginComponent.inject(this)
    ...
  }
}

Java

public class LoginActivity extends Activity {
    ...

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        loginComponent = ((LoginComponentProvider) getApplicationContext())
                                .provideLoginComponent();

        loginComponent.inject(this);

        ...
    }
}

現在,MyApplication 必須實作該介面,並實作必要方法:

Kotlin

class MyApplication: Application(), LoginComponentProvider {
  // Reference to the application graph that is used across the whole app
  val appComponent = DaggerApplicationComponent.create()

  override fun provideLoginComponent(): LoginComponent {
    return appComponent.loginComponent().create()
  }
}

Java

public class MyApplication extends Application implements LoginComponentProvider {
  // Reference to the application graph that is used across the whole app
  ApplicationComponent appComponent = DaggerApplicationComponent.create();

  @Override
  public LoginComponent provideLoginComponent() {
    return appComponent.loginComponent.create();
  }
}

如何在多模組專案中使用 Dagger 子元件。使用功能模組時,解決方案會因模組彼此的依賴方式而異。

功能模組的元件依附元件

使用功能模組時,模組間依附通常會依循反轉方式。功能模組不會使用包含功能模組的 app 模組,而是依附 app 模組。請參閱圖 2 以表示模組結構。

圖 2. 含有功能模組的專案中的 Dagger 圖表範例

在 Dagger 中,元件需要瞭解其子元件。這些資訊包含在已新增至父項元件的 Dagger 模組中 (例如在 Android 應用程式中使用 Dagger中的 SubcomponentsModule 模組)。

遺憾的是,由於應用程式和功能模組之間存在反向依附元件,app 模組不會顯示該子元件,因為子元件不在建構路徑中。舉例來說,在 login 功能模組中定義的 LoginComponent 不能是 app 模組中定義的 ApplicationComponent 子元件。

Dagger 有個機制稱為元件依附元件,您可以使用這個機制來解決這個問題。子項元件不是父項元件的子元件,而是取決於父項元件。因此,沒有上下層關係,現在,元件取決於其他元件,以取得特定依附元件。元件必須從圖表中呈現類型,相依元件才能取用。

例如,名為 login 的功能模組想要建構 LoginComponent,取決於 app Gradle 模組提供的 AppComponent

以下是類別與 app Gradle 模組的 AppComponent 的定義:

Kotlin

// UserRepository's dependencies
class UserLocalDataSource @Inject constructor() { ... }
class UserRemoteDataSource @Inject constructor() { ... }

// UserRepository is scoped to AppComponent
@Singleton
class UserRepository @Inject constructor(
    private val localDataSource: UserLocalDataSource,
    private val remoteDataSource: UserRemoteDataSource
) { ... }

@Singleton
@Component
interface AppComponent { ... }

Java

// UserRepository's dependencies
public class UserLocalDataSource {

    @Inject
    public UserLocalDataSource() {}
}

public class UserRemoteDataSource {

    @Inject
    public UserRemoteDataSource() { }
}

// UserRepository is scoped to AppComponent
@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;
    }
}

@Singleton
@Component
public interface ApplicationComponent { ... }

在包含 app Gradle 模組的 login Gradle 模組中,您有一個需要插入 LoginViewModel 執行個體的 LoginActivity

Kotlin

// LoginViewModel depends on UserRepository that is scoped to AppComponent
class LoginViewModel @Inject constructor(
    private val userRepository: UserRepository
) { ... }

Java

// LoginViewModel depends on UserRepository that is scoped to AppComponent
public class LoginViewModel {

    private final UserRepository userRepository;

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

LoginViewModel 有依附於 UserRepository 且範圍限制於 AppComponent 的依附元件。讓我們建立依附於 AppComponent 來插入 LoginActivityLoginComponent

Kotlin

// Use the dependencies attribute in the Component annotation to specify the
// dependencies of this Component
@Component(dependencies = [AppComponent::class])
interface LoginComponent {
    fun inject(activity: LoginActivity)
}

Java

// Use the dependencies attribute in the Component annotation to specify the
// dependencies of this Component
@Component(dependencies = AppComponent.class)
public interface LoginComponent {

    void inject(LoginActivity loginActivity);
}

LoginComponent 指定 AppComponent 的依附元件,透過將其新增至元件註解的依附元件參數。由於 LoginActivity 將由 Dagger 插入,因此請將 inject() 方法新增至介面。

建立 LoginComponent 時,必須傳入 AppComponent 的執行個體。請使用元件工廠進行這項操作:

Kotlin

@Component(dependencies = [AppComponent::class])
interface LoginComponent {

    @Component.Factory
    interface Factory {
        // Takes an instance of AppComponent when creating
        // an instance of LoginComponent
        fun create(appComponent: AppComponent): LoginComponent
    }

    fun inject(activity: LoginActivity)
}

Java

@Component(dependencies = AppComponent.class)
public interface LoginComponent {

    @Component.Factory
    interface Factory {
        // Takes an instance of AppComponent when creating
        // an instance of LoginComponent
        LoginComponent create(AppComponent appComponent);
    }

    void inject(LoginActivity loginActivity);
}

現在,LoginActivity 可以建立 LoginComponent 執行個體,並呼叫 inject() 方法。

Kotlin

class LoginActivity: Activity() {

    // You want Dagger to provide an instance of LoginViewModel from the Login graph
    @Inject lateinit var loginViewModel: LoginViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        // Gets appComponent from MyApplication available in the base Gradle module
        val appComponent = (applicationContext as MyApplication).appComponent

        // Creates a new instance of LoginComponent
        // Injects the component to populate the @Inject fields
        DaggerLoginComponent.factory().create(appComponent).inject(this)

        super.onCreate(savedInstanceState)

        // Now you can access loginViewModel
    }
}

Java

public class LoginActivity extends Activity {

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

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Gets appComponent from MyApplication available in the base Gradle module
        AppComponent appComponent = ((MyApplication) getApplicationContext()).appComponent;

        // Creates a new instance of LoginComponent
        // Injects the component to populate the @Inject fields
        DaggerLoginComponent.factory().create(appComponent).inject(this);

        // Now you can access loginViewModel
    }
}

LoginViewModel 取決於 UserRepository;為了讓 LoginComponent 能夠從 AppComponent 存取,AppComponent 必須在介面中公開:

Kotlin

@Singleton
@Component
interface AppComponent {
    fun userRepository(): UserRepository
}

Java

@Singleton
@Component
public interface AppComponent {
    UserRepository userRepository();
}

依附元件的限定範圍運作方式與子元件相同。由於 LoginComponent 使用 AppComponent 的執行個體,因此不能使用相同的範圍註解。

如要將 LoginViewModel 範圍限制為 LoginComponent,方法與先前使用自訂 @ActivityScope 註解相同。

Kotlin

@ActivityScope
@Component(dependencies = [AppComponent::class])
interface LoginComponent { ... }

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

Java

@ActivityScope
@Component(dependencies = AppComponent.class)
public interface LoginComponent { ... }

@ActivityScope
public class LoginViewModel {

    private final UserRepository userRepository;

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

最佳做法

  • ApplicationComponent 應一律位於 app 模組中。

  • 如果您需要在該模組中執行欄位插入,或需要針對應用程式的特定流程設定物件範圍,請在模組中建立 Dagger 元件。

  • 針對設計為公用程式或輔助程式且不需要建構圖形的 Gradle 模組 (這就是您需要 Dagger 元件的原因),請在不支援建構函式插入的類別中使用 @Provides 和 @Binds 建立並公開公開的 Dagger 模組。

  • 如要在具有功能模組的 Android 應用程式中使用 Dagger,請使用元件依附元件,以存取 app 模組中定義的 ApplicationComponent 提供的依附元件。