Android 推薦的應用程式架構建議將程式碼分成多個類別,以便與關注點分離;這個原則是階層中的每個類別都有一個已定義的責任。這會導致較小的類別必須彼此連接,以完成彼此的依附元件。

類別之間的依附元件可以表示為圖表,而圖表中每個類別都會連結至其所依附的類別。所有類別及其依附元件的表示方式構成了「應用程式圖表」。在圖 1 中,您可以看到應用程式圖表的抽象層。如果類別 A (ViewModel
) 依附於類別 B (Repository
),會有一條從 A 指向 B 的線代表依附元件。
作用依附元件有助於建立這類連線,並讓您替換實作以進行測試。舉例來說,在測試依附在存放區的 ViewModel
時,您可以透過傳遞不同的假的或測試的 Repository
實作來測試不同的案例。
手動依附元件插入的基本概念
本節說明如何在實際的 Android 應用程式情境中套用手動依附元件插入功能。此程式碼會逐步說明如何在應用程式中使用依附元件插入功能。該方法會不斷改善,直到與 Dagger 自動為您產生的內容非常類似為止。如要進一步瞭解 Dagger,請參閱「Dagger 基本概念」。
假設資料流是應用程式中一組與功能對應的畫面。登入、註冊和結帳等流程均屬流程的範例。
在說明一般 Android 應用程式的登入流程時,LoginActivity
取決於 LoginViewModel
,而後者則取決於 UserRepository
。然後,UserRepository
取決於 UserLocalDataSource
和 UserRemoteDataSource
,後者則取決於 Retrofit
服務。

LoginActivity
是登入流程的進入點,而使用者會與活動互動。因此,LoginActivity
必須建立所有依附元件的 LoginViewModel
。
流程的 Repository
和 DataSource
類別如下所示:
Kotlin
class UserRepository( private val localDataSource: UserLocalDataSource, private val remoteDataSource: UserRemoteDataSource ) { ... } class UserLocalDataSource { ... } class UserRemoteDataSource( private val loginService: LoginRetrofitService ) { ... }
Java
class UserLocalDataSource { public UserLocalDataSource() { } ... } class UserRemoteDataSource { private final Retrofit retrofit; public UserRemoteDataSource(Retrofit retrofit) { this.retrofit = retrofit; } ... } class UserRepository { private final UserLocalDataSource userLocalDataSource; private final UserRemoteDataSource userRemoteDataSource; public UserRepository(UserLocalDataSource userLocalDataSource, UserRemoteDataSource userRemoteDataSource) { this.userLocalDataSource = userLocalDataSource; this.userRemoteDataSource = userRemoteDataSource; } ... }
LoginActivity
的外觀如下:
Kotlin
class LoginActivity: Activity() { private lateinit var loginViewModel: LoginViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // In order to satisfy the dependencies of LoginViewModel, you have to also // satisfy the dependencies of all of its dependencies recursively. // First, create retrofit which is the dependency of UserRemoteDataSource val retrofit = Retrofit.Builder() .baseUrl("https://example.com") .build() .create(LoginService::class.java) // Then, satisfy the dependencies of UserRepository val remoteDataSource = UserRemoteDataSource(retrofit) val localDataSource = UserLocalDataSource() // Now you can create an instance of UserRepository that LoginViewModel needs val userRepository = UserRepository(localDataSource, remoteDataSource) // Lastly, create an instance of LoginViewModel with userRepository loginViewModel = LoginViewModel(userRepository) } }
Java
public class MainActivity extends Activity { private LoginViewModel loginViewModel; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // In order to satisfy the dependencies of LoginViewModel, you have to also // satisfy the dependencies of all of its dependencies recursively. // First, create retrofit which is the dependency of UserRemoteDataSource Retrofit retrofit = new Retrofit.Builder() .baseUrl("https://example.com") .build() .create(LoginService.class); // Then, satisfy the dependencies of UserRepository UserRemoteDataSource remoteDataSource = new UserRemoteDataSource(retrofit); UserLocalDataSource localDataSource = new UserLocalDataSource(); // Now you can create an instance of UserRepository that LoginViewModel needs UserRepository userRepository = new UserRepository(localDataSource, remoteDataSource); // Lastly, create an instance of LoginViewModel with userRepository loginViewModel = new LoginViewModel(userRepository); } }
這種方法發生的問題:
有很多樣板程式碼。如要在程式碼的其他部分建立另一個
LoginViewModel
的執行個體,則系統會複製程式碼。依附元件必須依序宣告。您必須在
LoginViewModel
之前將UserRepository
執行個體化,才能建立類別。無法重複使用物件。如果您想在多個功能中重複使用
UserRepository
,則必須採用單例模式。單例模式讓測試更加困難,因為所有測試都共用同一個單例模式執行個體。
透過容器管理依附元件
如要解決重複使用物件的問題,您可以建立自己的「依附元件容器」類別,用來取得依附元件。這個容器提供的所有執行個體皆可公開。在範例中,由於您只需要 UserRepository
的執行個體,因此您可以將依附元件設為不公開,如果日後需要提供公開,可以選擇將其設為公開:
Kotlin
// Container of objects shared across the whole app class AppContainer { // Since you want to expose userRepository out of the container, you need to satisfy // its dependencies as you did before private val retrofit = Retrofit.Builder() .baseUrl("https://example.com") .build() .create(LoginService::class.java) private val remoteDataSource = UserRemoteDataSource(retrofit) private val localDataSource = UserLocalDataSource() // userRepository is not private; it'll be exposed val userRepository = UserRepository(localDataSource, remoteDataSource) }
Java
// Container of objects shared across the whole app public class AppContainer { // Since you want to expose userRepository out of the container, you need to satisfy // its dependencies as you did before private Retrofit retrofit = new Retrofit.Builder() .baseUrl("https://example.com") .build() .create(LoginService.class); private UserRemoteDataSource remoteDataSource = new UserRemoteDataSource(retrofit); private UserLocalDataSource localDataSource = new UserLocalDataSource(); // userRepository is not private; it'll be exposed public UserRepository userRepository = new UserRepository(localDataSource, remoteDataSource); }
由於這些依附元件是在整個應用程式中使用,因此必須放在所有活動可用的通用位置中:Application
類別。建立自訂包含 AppContainer
執行個體的 Application
類別。
Kotlin
// Custom Application class that needs to be specified // in the AndroidManifest.xml file class MyApplication : Application() { // Instance of AppContainer that will be used by all the Activities of the app val appContainer = AppContainer() }
Java
// Custom Application class that needs to be specified // in the AndroidManifest.xml file public class MyApplication extends Application { // Instance of AppContainer that will be used by all the Activities of the app public AppContainer appContainer = new AppContainer(); }
您現在可以從應用程式取得 AppContainer
的執行個體並取得 UserRepository
的共用:
Kotlin
class LoginActivity: Activity() { private lateinit var loginViewModel: LoginViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Gets userRepository from the instance of AppContainer in Application val appContainer = (application as MyApplication).appContainer loginViewModel = LoginViewModel(appContainer.userRepository) } }
Java
public class MainActivity extends Activity { private LoginViewModel loginViewModel; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // Gets userRepository from the instance of AppContainer in Application AppContainer appContainer = ((MyApplication) getApplication()).appContainer; loginViewModel = new LoginViewModel(appContainer.userRepository); } }
這樣一來,您沒有單例模式 UserRepository
。反之,您針對包含圖中物件的所有活動共用 AppContainer
,並建立可供其他類別使用的,這些物件的執行個體。
如果應用程式中的更多位置需要 LoginViewModel
,則需要能夠集中建立 LoginViewModel
執行個體的集中位置。您可以將 LoginViewModel
的建立移至容器,並用工廠提供該類型的新物件。LoginViewModelFactory
的程式碼如下所示:
Kotlin
// Definition of a Factory interface with a function to create objects of a type interface Factory<T> { fun create(): T } // Factory for LoginViewModel. // Since LoginViewModel depends on UserRepository, in order to create instances of // LoginViewModel, you need an instance of UserRepository that you pass as a parameter. class LoginViewModelFactory(private val userRepository: UserRepository) : Factory{ override fun create(): LoginViewModel { return LoginViewModel(userRepository) } }
Java
// Definition of a Factory interface with a function to create objects of a type public interface Factory<T> { T create(); } // Factory for LoginViewModel. // Since LoginViewModel depends on UserRepository, in order to create instances of // LoginViewModel, you need an instance of UserRepository that you pass as a parameter. class LoginViewModelFactory implements Factory{ private final UserRepository userRepository; public LoginViewModelFactory(UserRepository userRepository) { this.userRepository = userRepository; } @Override public LoginViewModel create() { return new LoginViewModel(userRepository); } }
您可以在 AppContainer
中加入 LoginViewModelFactory
,讓 LoginActivity
使用這個程式碼:
Kotlin
// AppContainer can now provide instances of LoginViewModel with LoginViewModelFactory class AppContainer { ... val userRepository = UserRepository(localDataSource, remoteDataSource) val loginViewModelFactory = LoginViewModelFactory(userRepository) } class LoginActivity: Activity() { private lateinit var loginViewModel: LoginViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Gets LoginViewModelFactory from the application instance of AppContainer // to create a new LoginViewModel instance val appContainer = (application as MyApplication).appContainer loginViewModel = appContainer.loginViewModelFactory.create() } }
Java
// AppContainer can now provide instances of LoginViewModel with LoginViewModelFactory public class AppContainer { ... public UserRepository userRepository = new UserRepository(localDataSource, remoteDataSource); public LoginViewModelFactory loginViewModelFactory = new LoginViewModelFactory(userRepository); } public class MainActivity extends Activity { private LoginViewModel loginViewModel; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // Gets LoginViewModelFactory from the application instance of AppContainer // to create a new LoginViewModel instance AppContainer appContainer = ((MyApplication) getApplication()).appContainer; loginViewModel = appContainer.loginViewModelFactory.create(); } }
這種方法比前一個方法更好,但還是有一些需要考量的挑戰:
您必須自行管理
AppContainer
,以便手動建立所有依附元件的執行個體。仍然有許多樣板程式碼。您必須依據是否要重複使用物件,手動建立工廠或參數。
在應用程式流程中管理依附元件
如果您要在專案中加入更多功能,AppContainer
會很複雜。隨著應用程式規模變大,並開始引進不同的功能流程時,會產生更多的問題:
如果有不同的流程,您可能會想要物件僅在該流程的範圍內。例如,建立
LoginUserData
(可能包括登入流程中使用的使用者名稱和密碼) 時,您不想保留來自不同使用者的舊登入流程的資料。您希望每個新流程建立新的執行個體。方法是在AppContainer
中建立FlowContainer
物件,如下一個程式碼範例所示。應用程式圖表和流程容器的最佳化也可以是困難的。必須記得刪除不需要的執行個體,視所在的流程而定。
假設您有一個登入流程,當中包含一個活動 (LoginActivity
) 和多個片段 (LoginUsernameFragment
和 LoginPasswordFragment
)。這些檢視畫面想要:
存取需要共用的相同
LoginUserData
執行個體,直到登入流程完成為止。在流程再次啟動時,建立新的
LoginUserData
執行個體。
只要使用登入流程容器就能完成這項工作。必須在登入流程開始時建立此容器,並在流程結束時從記憶體中移除。
現在,請在程式碼範例中新增 LoginContainer
。您會想在應用程式中建立多個 LoginContainer
的執行個體,因此,請將應用程式設為來自 AppContainer
登入流程所需的依附元件的類別,而非將其設為單例模式。
Kotlin
class LoginContainer(val userRepository: UserRepository) { val loginData = LoginUserData() val loginViewModelFactory = LoginViewModelFactory(userRepository) } // AppContainer contains LoginContainer now class AppContainer { ... val userRepository = UserRepository(localDataSource, remoteDataSource) // LoginContainer will be null when the user is NOT in the login flow var loginContainer: LoginContainer? = null }
Java
// Container with Login-specific dependencies class LoginContainer { private final UserRepository userRepository; public LoginContainer(UserRepository userRepository) { this.userRepository = userRepository; loginViewModelFactory = new LoginViewModelFactory(userRepository); } public LoginUserData loginData = new LoginUserData(); public LoginViewModelFactory loginViewModelFactory; } // AppContainer contains LoginContainer now public class AppContainer { ... public UserRepository userRepository = new UserRepository(localDataSource, remoteDataSource); // LoginContainer will be null when the user is NOT in the login flow public LoginContainer loginContainer; }
擁有資料流的容器後,您必須決定何時建立及刪除容器執行個體。因為登入流程是活動 (LoginActivity
) 中的獨立內容,因此是由活動管理該容器的生命週期。LoginActivity
可在 onCreate()
中建立執行個體,並在 onDestroy()
中刪除該執行個體。
Kotlin
class LoginActivity: Activity() { private lateinit var loginViewModel: LoginViewModel private lateinit var loginData: LoginUserData private lateinit var appContainer: AppContainer override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) appContainer = (application as MyApplication).appContainer // Login flow has started. Populate loginContainer in AppContainer appContainer.loginContainer = LoginContainer(appContainer.userRepository) loginViewModel = appContainer.loginContainer.loginViewModelFactory.create() loginData = appContainer.loginContainer.loginData } override fun onDestroy() { // Login flow is finishing // Removing the instance of loginContainer in the AppContainer appContainer.loginContainer = null super.onDestroy() } }
Java
public class LoginActivity extends Activity { private LoginViewModel loginViewModel; private LoginData loginData; private AppContainer appContainer; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); appContainer = ((MyApplication) getApplication()).appContainer; // Login flow has started. Populate loginContainer in AppContainer appContainer.loginContainer = new LoginContainer(appContainer.userRepository); loginViewModel = appContainer.loginContainer.loginViewModelFactory.create(); loginData = appContainer.loginContainer.loginData; } @Override protected void onDestroy() { // Login flow is finishing // Removing the instance of loginContainer in the AppContainer appContainer.loginContainer = null; super.onDestroy(); } }
與 LoginActivity
一樣,登入片段可從 AppContainer
存取 LoginContainer
,並使用共用的 LoginUserData
執行個體。
在本範例中,您要處理檢視生命週期邏輯,因此使用生命週期觀察才是合理的。
結語
依附元件插入是建立可擴充且可測試的 Android 應用程式的好技巧。使用容器做為在應用程式不同部分共用執行個體的類別,以及使用工廠建立類別的執行個體的集中位置。
隨著應用程式的規模變大,您會開始看到您寫出許多樣板程式碼 (例如工廠),而容易出錯。您也必須自行管理容器的範圍和生命週期,對不再需要的容器進行最佳化調整和丟棄,以便釋出記憶體。此動作出錯可能會導致應用程式出現細微錯誤和記憶體流失。
在「Dagger」部分,您將瞭解如何使用 Dagger 自動執行這項程序,並產生您以手動方式編寫的程式碼。