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 類別如下所示:
class UserRepository(
private val localDataSource: UserLocalDataSource,
private val remoteDataSource: UserRemoteDataSource
) { ... }
class UserLocalDataSource { ... }
class UserRemoteDataSource(
private val loginService: LoginRetrofitService
) { ... }
在 Compose 中,ComponentActivity 是進入點;依附元件連線會在 onCreate 中發生一次,而 UI 則是由從 setContent 呼叫的可組合函式描述:
class ApiService {
/* Your API implementation here */
}
class UserRepository(private val apiService: ApiService) {
/* Your implementation here */
}
class LoginActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Satisfy the dependencies of LoginViewModel recursively,
// then pass what the UI needs into setContent.
val apiService = ApiService()
val userRepository = UserRepository(apiService)
setContent {
LoginScreen(userRepository)
}
}
}
@Composable
fun LoginScreen(userRepository: UserRepository) {
val viewModel: LoginViewModel = viewModel(
factory = LoginViewModelFactory(userRepository)
)
// ...
}
這種做法會產生以下問題:
- 必須依序宣告依附元件。您必須先建立
UserRepository的例項,才能建立LoginViewModel。 - 要重複使用物件十分困難。如果想在多項功能中重複使用
UserRepository,就必須依循單例模式。在單例模式下,所有測試都會共用同一個單例模式例項,使得測試更加困難。
透過容器管理依附元件
如要解決重複使用物件的問題,可以自行建立「依附元件容器」類別,用來取得依附元件。這個容器提供的所有例項皆可設為公開。在本例中只需要一個 UserRepository 的例項,因此您可以將其依附元件設為不公開,日後如有需要,還是可以設為公開:
// Container of objects shared across the whole app
class AppContainer {
// apiService and userRepository aren't private and will be exposed
val apiService = ApiService()
val userRepository = UserRepository(apiService)
}
這些依附元件會用於整個應用程式,因此需要放在所有活動都能使用的共同位置,也就是 Application 類別。建立包含 AppContainer 例項的自訂 Application 類別。
// 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()
}
使用 Compose 時,系統仍會在 Application 子類別中建立相同的 AppContainer。您可以在活動中呼叫 setContent 之前存取,也可以使用 LocalContext 從可組合函式中存取:
class LoginActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val appContainer = (application as MyApplication).appContainer
setContent {
LoginScreen(appContainer.userRepository)
}
}
}
// Alternatively, read AppContainer from inside a composable:
@Composable
fun LoginScreen() {
val context = LocalContext.current
val appContainer = (context.applicationContext as MyApplication).appContainer
val viewModel: LoginViewModel = viewModel(
factory = LoginViewModelFactory(appContainer.userRepository)
)
// ...
}
建議您將依附元件做為可組合的參數傳遞,而不是從樹狀結構深處存取 LocalContext。這樣一來,可組合項就能進行測試,且輸入內容明確。在畫面根層級解析容器一次,並向下傳遞所需內容。
這樣一來,您就沒有採用單例模式的 UserRepository,而是擁有所有活動共用的 AppContainer,這些活動包含圖表內的物件,且會建立這些物件的例項供其他類別使用。
如果應用程式有更多地方需要使用 LoginViewModel,就有理由集中一處建立 LoginViewModel 的例項。您可以將 LoginViewModel 的建立程序移至容器,並使用工廠提供該類型的新物件。用於 LoginViewModelFactory 的程式碼如下所示:
// 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<LoginViewModel> {
override fun create(): LoginViewModel {
return LoginViewModel(userRepository)
}
}
使用 Compose 時,AppContainer 更新仍會公開工廠。接著,viewModel 可組合函式會使用工廠,因此 ViewModel 會限定在最接近的 ViewModelStoreOwner 範圍內 (通常是主機活動,或是使用 Navigation Compose 時的導覽項目):
// AppContainer exposing the factory (unchanged from the snippet above)
class AppContainer {
// ...
val userRepository = UserRepository(localDataSource, remoteDataSource)
val loginViewModelFactory = LoginViewModelFactory(userRepository)
}
// Compose entry point + screen composable
class LoginActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val appContainer = (application as MyApplication).appContainer
setContent {
LoginScreen(appContainer.loginViewModelFactory)
}
}
}
@Composable
fun LoginScreen(factory: LoginViewModelFactory) {
val viewModel: LoginViewModel = viewModel(factory = factory)
// ...
}
這種做法比先前的好,但還有一些需要考量的難題:
您必須自行管理
AppContainer,手動建立所有依附元件的例項。仍有許多樣板程式碼。您需根據是否要重複使用物件,手動建立工廠或參數。
在應用程式流程中管理依附元件
如果要在專案中加入更多功能,AppContainer 會變得越來越複雜。隨著應用程式規模變大,並開始導入不同的功能流程,會產生更多問題:
如果有多個不同的流程,您可能會想讓物件僅在該流程的範圍內運作。舉例來說,建立
LoginUserData(可能包含僅在登入流程中使用的使用者名稱和密碼) 時,您不想保留舊登入流程中不同使用者提供的資料。如果希望每個新流程都使用全新的例項,可以在AppContainer內建立FlowContainer物件,如下一個程式碼範例所示。最佳化應用程式圖表和流程容器也很困難。您需要記得根據個別流程,刪除不必要的例項。
現在,我們要在範例程式碼中新增 LoginContainer。您希望在應用程式中建立多個 LoginContainer 的例項,因此請將其設為包含 AppContainer 中登入流程所需依附元件的類別,而非設為單例模式。
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
}
在 Compose 中,流程容器的生命週期會與組合項繫結,而非主機 Activity。您不需要變動共用 AppContainer.loginContainer,因為可組合項會將依附元件做為參數接收,或從提升的 ViewModel 讀取依附元件。方法有以下兩種:
- Navigation Compose 巢狀圖 (適用於多畫面流程)。
將登入流程中的所有畫面放在巢狀導覽圖下方,並將容器範圍設為該圖的
NavBackStackEntry。使用者進入流程時會建立容器,返回堆疊項目彈出時則會清除容器,無須手動呼叫生命週期。詳情請參閱「設計導覽圖」。 remember位於畫面根層級 (適用於單一畫面流程,或未使用 Navigation Compose 時)。在remember內建構容器,以便在每次進入組合時建立容器,並在可組合函式離開時進行垃圾收集:
@Composable
fun LoginFlow(appContainer: AppContainer) {
val loginContainer = remember(appContainer) {
LoginContainer(appContainer.userRepository)
}
val viewModel: LoginViewModel = viewModel(
factory = loginContainer.loginViewModelFactory
)
// Render the login flow using loginContainer.loginData and viewModel.
}
結論
建立可擴充且可測試的 Android 應用程式時,依附元件插入是一項好用的功能。利用容器在應用程式不同部分共用類別的例項,並使用工廠集中一處建立類別的例項。
隨著應用程式規模變大,您會發現自己編寫出許多容易出錯的樣板程式碼 (例如工廠)。您也必須自行管理容器的範圍和生命週期,最佳化及捨棄不再需要的容器來釋出記憶體。如果出錯,可能會導致應用程式出現細微錯誤和記憶體流失。
您可以參閱「Dagger」相關章節,瞭解如何使用 Dagger 自動執行這項程序,並透過其他方式產生以手動方式編寫的相同程式碼。