开发思想: 对象复用
The ANDROID recommended application architecture recommends breaking code into multiple classes to benefit from the principle of Separation of concerns, where each class of the hierarchy has a defined responsibility. This requires wiring together more and smaller classes to achieve dependencies on each other.
[Android 推荐应用架构]
建议将代码划分为多个类,以从分离关注点这一原则(其中,层次结构的每个类都具有一项已定义的责任)中受益。这就需要将更多更小的类连接在一起,以实现彼此之间的依赖关系。
各个类之间的依赖关系可以表示为图表,其中每个类都连接到其所依赖的类。所有类及其依赖关系的表示法便构成了应用图表。在图 1 中,您可以看到应用图表的抽象呈现。当 A 类 (ViewModel) 依赖于 B 类 (Repository) 时,有一条从 A 指向 B 的直线表示该依赖关系。
The dependencies between classes can be represented as diagrams, where each class is connected to the class on which it depends. The representation of all classes and their dependencies makes up the application diagram. In figure 1, you can see an abstract rendering of the application diagram. When Class A (ViewModel) is dependent on class B (Repository) , a line from A to B indicates this dependency.
依赖项注入有助于建立这些链接并使您可以更换实现以进行测试。例如,在测试依赖于代码库的 ViewModel 时,您可以通过伪造或模拟传递 Repository 的不同实现,以测试不同的情形。
Dependency injection helps establish these links and allows you to replace the implementation for testing. For example, when testing viewmodels that depend on the code base, you can test different situations by passing different implementations of the Repository through forgery or emulation.
在介绍典型 Android 应用的登录流程时,LoginActivity
依赖于 LoginViewModel
,而后者又依赖于 UserRepository
。然后,UserRepository
依赖于 UserLocalDataSource
和 UserRemoteDataSource
,而后者又依赖于 Retrofit
服务。
When describing the login process for a typical Android application, the LoginActivity relies on the LOGINVIEWMODEL, which in turn relies on the UserRepository. The userdatasource repository then relies on the UserLocalDataSource and the UserRemoteDataSource, which in turn depends on the Retrofit service.
LoginActivity 是登录流程的入口点,用户与 Activity 进行交互。因此,LoginActivity 需要创建 LoginViewModel 及其所有依赖项。
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 如下所示:
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
,必须使其遵循单例模式。单例模式使测试变得更加困难,因为所有测试共享相同的单例实例。
该流程的 Repository 和 DataSource 类如下所示:
使用容器管理依赖项
如需解决重复使用对象的问题,您可以创建自己的依赖项容器类,用于获取依赖项。此容器提供的所有实例可以是公共实例。在该示例中,由于您仅需要 UserRepository 的一个实例,您可以将其依赖项设为私有,并且可以在将来需要提供依赖项时将其公开:
// 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);
}
由于这些依赖项在整个应用中使用,因此需要将它们放置在所有 Activity 都可以使用的通用位置:应用类。创建一个包含 AppContainer 实例的自定义应用类。
// 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 实例:
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。相反,您可以在所有 Activity *享 AppContainer,其包含图表中的对象并创建其他类可以使用的对象实例。
多处复用
如果需要在应用的更多位置使用 LoginViewModel,则具有一个可创建 LoginViewModel 实例的集中位置是有必要的。您可以将 LoginViewModel 的创建移至容器,并为工厂提供该类型的新对象。LoginViewModelFactory 的代码如下所示:
// Definition of a Factory interface with a function to create objects of a type
public interface Factory {
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 使用它:
// 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();
}
}
此方法比前一种方法更好,但仍需考虑一些挑战:
This approach is better than the former, but there are challenges to consider:
1.您必须自行管理 AppContainer,手动为所有依赖项创建实例。
You must manage the AppContainer by yourself, creating instances for all dependencies manually.
2.仍然有大量样板代码。您需要手动创建工厂或参数,具体取决于是否要重复使用某个对象。
There’s still a lot of boilerplate. You need to manually create a factory or parameter, depending on whether you want to reuse an object.
管理应用流程中的依赖项
如需在项目中添加更多功能,AppContainer 会变得非常复杂。当应用变大并且可以引入不同功能流程时,还会出现更多问题:
当您具有不同的流程时,您可能希望对象仅位于该流程的范围内。例如,在创建 LoginUserData 时(可能包含仅在登录流程中使用的用户名和密码),您不希望保留来自其他用户的旧登录流程中的数据。您需要为每个新流程创建一个新实例。您可以通过在 AppContainer 内部创建 FlowContainer 对象实现这一目标,如下面的代码示例所示。
访问需要共享的同一 LoginUserData 实例,直至登录流程完成。
Access the same LoginUserData instance that needs to be shared until the login process is complete.
当该流程再次开始时,创建一个新的 LoginUserData 实例。
When the process starts again, create a new instance of Loginuserdata.
您可以使用登录流程容器实现这一目标。此容器需要在登录流程开始时创建,并在流程结束时将其从内存中移除。
You can do this using the login process container. This container needs to be created at the beginning of the login process and removed from memory at the end of the process.
我们将 LoginContainer 添加到示例代码中。您希望能够在应用中创建多个 LoginContainer 实例,因此,请不要将其设为单例,而应使其成为具有登录流程需要从 AppContainer 中获取的依赖项的类。
We add the LoginContainer to the sample code. You want to be able to create multiple LoginContainer instances in your application, so don’t make it a Singleton, make it a class with dependencies that the login process needs to get from AppContainer.
// 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;
}
拥有某个流程专用的容器后,必须决定何时创建和删除容器实例。由于您的登录流程在 Activity (LoginActivity) 中是独立的,因此该 Activity 是管理该容器生命周期的 Activity。LoginActivity 可以在 onCreate() 中创建实例并在 onDestroy() 中将其删除。
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 一样,登录 Fragment 可以从 AppContainer 访问 LoginContainer 并使用共享的 LoginUserData 实例。