Android 的推荐应用架构 鼓励将您的代码划分为不同的类,以从关注点分离中获益,关注点分离原则要求层次结构中的每个类都具有一个定义明确的责任。这会导致更多更小的类需要连接在一起以满足彼此的依赖关系。
类之间的依赖关系可以用图表示,其中每个类都与其依赖的类相连。所有类及其依赖关系的表示构成了应用图。在图 1 中,您可以看到应用图的抽象。当类 A (ViewModel
) 依赖于类 B (Repository
) 时,有一条从 A 指向 B 的线代表该依赖关系。
依赖注入有助于建立这些连接,并使您能够交换实现以进行测试。例如,在测试依赖于存储库的ViewModel
时,您可以传递不同的Repository
实现,这些实现使用模拟或存根,以测试不同的情况。
手动依赖注入的基础知识
本节介绍如何在真实的 Android 应用场景中应用手动依赖注入。它将逐步介绍如何在应用中开始使用依赖注入的方法。这种方法会不断改进,直到达到非常类似于 Dagger 自动为您生成的程度。有关 Dagger 的更多信息,请阅读Dagger 基础知识。
将流程视为应用中的一组屏幕,这些屏幕对应于某个功能。登录、注册和结账都是流程的例子。
在介绍典型 Android 应用的登录流程时,LoginActivity
依赖于 LoginViewModel
,而 LoginViewModel
又依赖于 UserRepository
。然后,UserRepository
依赖于 UserLocalDataSource
和 UserRemoteDataSource
,而 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 自动执行此过程,并生成你原本需要手动编写的相同代码。