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
的另一个实例,则会发生代码重复。必须按顺序声明依赖项。您必须先实例化
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); } }
您可以将LoginViewModelFactory
包含在AppContainer
中,并使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 自动执行此过程并生成您本来需要手动编写的相同代码。