Android 建議的應用程式架構建議將程式碼分割為多個類別,這麼做可享有關注點分離原則帶來的好處;這項原則是指階層的每個類別都具有已定義的單一責任。這麼做需要將更多較小的類別互相連結,成為彼此的依附元件。
類別之間的依附元件能以圖表形式呈現,其中每個類別都會分別連結至依附的類別。所有類別及其依附元件的呈現方式可構成「應用程式圖表」。圖 1 顯示應用程式圖表的摘要。當類別 A (ViewModel
) 依附類別 B (Repository
) 時,從 A 指向 B 的線條就代表該依附元件。
依附元件插入功能可以建立這類連結,並替換用於測試的實作內容。舉例來說,測試依附存放區的 ViewModel
時,您可以透過假的或模擬的方式,傳遞不同的 Repository
實作內容來測試各種情況。
手動插入依附元件基礎
本節說明如何在 Android 應用程式的實際情境中套用手動依附元件插入功能,並逐步說明如何透過疊代的做法,在應用程式中使用依附元件插入功能。這種做法會不斷改進,直到達到接近 Dagger 自動產生的效果。如要進一步瞭解 Dagger,請參閱「Dagger 基本概念」。
假設流程為應用程式中對應至特定功能的一組畫面。登入、註冊和結帳都是流程的範例。
在一般 Android 應用程式的登入流程中,LoginActivity
會依附 LoginViewModel
,而後者則依附 UserRepository
。其後,UserRepository
依附 UserLocalDataSource
和
UserRemoteDataSource
,而後者依附 Retrofit
課程中也會快速介紹 Memorystore
這是 Google Cloud 的全代管 Redis 服務
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
例項,就會產生重複的程式碼。必須依序宣告依附元件。您必須先建立
UserRepository
的例項,才能建立LoginViewModel
。要重複使用物件十分困難。如果想在多項功能中重複使用
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 自動執行這項程序,並透過其他方式產生以手動方式編寫的相同程式碼。