含有多個 Gradle 模組的專案稱為多模組專案。如果是以單一 APK 形式提供,且不含功能模組的多模組專案,通常會具有可依附於專案中大部分模組的 app
模組,以及其餘模組通常會依附的 base
或 core
模組。app
模組通常包含 Application
類別,而 base
模組則包含專案中所有模組共用的所有通用類別。
app
模組是適合宣告應用程式元件 (如下圖中 ApplicationComponent
) 的位置,可藉此提供其他元件和應用程式單例模式可能需要的物件。舉例來說,app
模組中定義的 ApplicationComponent
會提供 OkHttpClient
等類別、JSON 剖析器、資料庫的存取子,或是可在 core
模組中定義的 SharedPreferences
物件。
在 app
模組中,也可以有其他生命週期較短的元件,例如在登入後具有使用者專屬設定 (如 UserSession
) 的 UserComponent
。
在專案的不同模組中,您可以定義至少一項具有該模組專屬邏輯的子元件,如圖 1 所示。
舉例來說,在 login
模組中,您可以將 LoginComponent
的範圍指定為自訂 @ModuleScope
註解,該註解可提供該功能的通用物件,例如 LoginRepository
。在該模組中,也可以有其他依附於 LoginComponent
但自訂範圍不同的元件,例如 LoginActivityComponent
的 @FeatureScope
,或是範圍可涵蓋更多功能專屬邏輯的 TermsAndConditionsComponent
,例如 ViewModel
物件。
若是 Registration
等其他模組,可以使用類似設定。
多模組專案的通則是,相同層級的模組不應彼此依附。如果這類模組彼此依附,請考量該共用邏輯 (兩者之間的依附元件) 是否應屬於父項模組。若是,請重構並將類別移至父項模組,否則請建立新模組來擴充父項模組,並讓兩個原始模組擴充新模組。
如要在下列情況採取最佳做法,您通常會在模組中建立元件:
您需要執行欄位插入作業,就像使用
LoginActivityComponent
一樣。您需要設定物件範圍,就像使用
LoginComponent
一樣。
如果上述兩種情況均不適用,而您需要告知 Dagger 如何提供該模組的物件,請使用 @Provides
或 @Binds
方法建立及公開 Dagger 模組,前提是這些類別無法執行建構函式插入作業。
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
模組不瞭解 MyApplication
和 appComponent
。如要順利運作,您需要在功能模組中定義介面,提供 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 中的模組結構示意圖。
在 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
的 LoginComponent
,以插入 LoginActivity
:
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
所提供的依附元件。