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。
在 Android 中,您通常會建立一個位於應用程式類別中的 Dagger 圖形,因為您希望應用程式執行時,圖表的執行個體位於記憶體中。這樣一來,圖表就會連結至應用程式生命週期。在某些情況下,您可能也希望在圖表中應用程式結構定義可供使用。因此,您的圖表必須屬於 Application
類別。這種做法的其中一個優點是,其他 Android 架構類別可以使用該圖表。此外,還可以在測試中使用自訂 Application
類別,以簡化測試作業。
由於產生圖表的介面已加上 @Component
註解,因此您可以呼叫 ApplicationComponent
或 ApplicationGraph
。您通常會將該元件的執行個體保留在自訂 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
具有其依附元件)。如果您有多個要求插入的類別,則必須在元件中明確宣告這些類型的確切類型。舉例來說,如果您要求插入 LoginActivity
和 RegistrationActivity
,您會有兩個 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
。由於 LoginActivity
插入了 LoginViewModel
,所以 Dagger 建構的圖表會知道如何提供 LoginViewModel
的執行個體,並遞迴地提供其依附元件。由於類別建構函式上的 @Inject
註解,Dagger 知道如何執行這項操作。
在 Dagger 產生的 ApplicationComponent
中,有一個工廠類型的方法,可取得其已知如何提供的所有類別的執行個體。在此範例中,Dagger 委派至 ApplicationComponent
中包含的 NetworkModule
,以取得 LoginRetrofitService
的執行個體。
Dagger 範圍
已在Dagger 基本概念頁面中提及範圍,以便在元件中擁有特定類型的專屬執行個體。也就是「限定元件的生命週期的類型」。
由於您可能會想在應用程式的其他功能中使用 UserRepository
,且可能不會在每次需要時建立新物件,因此可以將其指定為整個應用程式的專屬執行個體。這是與 LoginRetrofitService
相同的執行個體:建立費用可能相當昂貴,而且還需要該物件的專屬執行個體可以重複使用。建立 UserRemoteDataSource
執行個體並不昂貴,因此,限定元件的生命週期的類型並不必要。
@Singleton
是 javax.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
加上註解,以重複使用執行個體,原因如下:
完成流程後,
LoginViewModel
的執行個體會保留在記憶體中。希望每個登入流程使用不同的
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 LoginComponent
是 ApplicationComponent
的子元件,您必須透過下列方式指示:
建立新的 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 { }
將新模組 (例如
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
的執行個體。父項元件必須在其介面中新增方法,讓客戶能夠從父項元件的執行個體中建立子元件的執行個體:公開在介面中建立
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(); }
將範圍指派給子元件
當您建構專案時,可以建立 ApplicationComponent
和 LoginComponent
的執行個體。因您希望只要應用程式在記憶體中,就使用相同的圖表的執行個體,因此將 ApplicationComponent
附加至應用程式的生命週期。
LoginComponent
的生命週期為何?需要使用 LoginComponent
的其中一個原因是,您需要在登入相關片段之間共用相同的 LoginViewModel
執行個體。此外,每當有新的登入流程時,您希望不同的 LoginViewModel
執行個體。LoginActivity
是 LoginComponent
的正確生命週期:針對每個新活動,您需要新的 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
執行個體。您可以建立自訂註解範圍,並為其加上 LoginComponent
和 LoginViewModel
註解,藉此確保這一點。請注意,由於 @Singleton
註解已用於父項元件,因此會將物件設為應用程式單例模式 (整個應用程式的不重複執行個體),因此無法使用。您必須建立其他註解範圍。
在此情況下,您可以命名此範圍為 @LoginScope
,但我們不建議這麼做。範圍註解的名稱不應明確符合其用途。請改為根據其生命週期命名,因為 RegistrationComponent
和 SettingsComponent
等同層級元件可以重複使用註解。因此建議您命名其為 @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
,這兩個片段都會有相同的執行個體。舉例來說,如果您有 LoginUsernameFragment
和 LoginPasswordFragment
,則 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 圖表外觀。標有白點的類別 (UserRepository
、LoginRetrofitService
和 LoginViewModel
) 是各自元件中具有不重複執行個體範圍的類別。
讓我們來解析圖表的各個部分:
ApplicationComponent
中已納入NetworkModule
(因此為LoginRetrofitService
),因為您已在元件中指定。UserRepository
保留在ApplicationComponent
中,因為其範圍為ApplicationComponent
。如果專案成長,您希望在不同功能 (例如註冊) 之間共用相同的執行個體。因為
UserRepository
為ApplicationComponent
的一部分,其依附元件 (例如UserLocalDataSource
和UserRemoteDataSource
) 也必須加入這個元件,才能提供UserRepository
的執行個體。LoginComponent
中包含LoginViewModel
,因為LoginComponent
插入的類別才需使用。LoginViewModel
未包含在ApplicationComponent
,因為ApplicationComponent
中沒有任何依附元件需要LoginViewModel
。同樣地,如果您尚未將
UserRepository
範圍設為ApplicationComponent
,Dagger 會自動包含UserRepository
及其依附元件做為LoginComponent
的一部分,因為這是目前唯一使用UserRepository
的地方。
除了將物件範圍限定為不同的生命週期,建立子元件也是封裝應用程式不同部分的最佳做法。
建構應用程式以根據應用程式流程建立不同的 Dagger 子圖表,有助於在記憶體和啟動時間方面取得更高效能且可擴充的應用程式。
建構 Dagger 圖表的最佳做法
為應用程式建構 Dagger 圖時:
建立元件時,應考量影響元件生命週期的元素為何。在這種情況下,
Application
類別負責ApplicationComponent
,LoginActivity
則負責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 { }
FakeNetworkModule
有 NetworkModule
原始版本的假實作。您可以在其中提供要替換的假執行個體或模擬圖。
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(); } }
在整合或端對端測試中,您將使用建立 TestApplicationComponent
的 TestApplication
而非 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
包含 Module1
和 Module2
,而 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):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):來自 ModuleX
中 Module1
和 Module2
常見的依附元件,解壓縮到在元件中名為 ModuleXCommon
的新模組。然後透過每個模組專屬的依附元件來建立這兩個名為 ModuleXWithModule1Dependencies
和 ModuleXWithModule2Dependencies
的其他模組。在 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」。