官方文档链接:https://developer.android.google.cn/topic/libraries/architecture/guide.html
1.前言
由于用户在使用某一功能时会涉及不同的应用程序,需要不断地切换流程和任务。举个例子,在使用社交软件分享照片时,会需要打开摄像头,也会需要从图库中选择文件,而将数据返回社交软件的过程中,有可能会有电话打来。这一下子启动了许多应用程序,但是移动设备资源有限,操作系统随时会杀死一些应用程序来腾出空间给新的应用。所以应用程序组件的存在不由开发者控制,不应该存储数据或状态,更不应该彼此依赖。
之前通过生命周期进行相关操作,但这引入另一套逻辑,增加代码的复杂度。
2.常见架构原则
2.1.View层分离
使Activity和Fragment中的代码尽可能的少,只处理与界面或与操作系统的交互。因为这些类是操作系统和应用程序的中间件,不由开发者控制,所以应该最小化依赖。
2.2.Model层驱动
通过持久化模型驱动界面,具有两点好处:
- 当操作系统释放组件资源时,用户数据也不会丢失;
- 当网络连接状态不好时,应用程序也能正常工作。
持久化模型独立于组件,不受系统的控制。基于它的应用程序可以保证界面代码简单,同时业务逻辑具有可测试性。
3.推荐的应用框架
没有一种架构适用于所有场景。意味着,这是一个好的起点,但若已经有更适合的了,是不需要改变的。以网络获取并显示用户配置信息为例子,展示架构组件的使用方式。
3.1.建立用户界面
界面包含一个UserProfileFragment.java
的组件类和user_profile_layout.xml
的布局文件。为了驱动界面,基于ViewModel类创建UserProfileViewModel.java
的数据模型来保存信息。数据元素主要包括:
- The User ID:用户标识符。最好通过
Fragment Arguments
传入Fragment,那样操作系统销毁应用进程时,id将被保存以便重启时使用。 - The User object:一个POJO类保存用户数据。
一个ViewModel给一个特定的界面组件(Activity或Fragment)提供数据,并处理业务逻辑中数据相关的操作,如调用其它组件来加载数据或转发用户的修改。ViewModel与View隔离,不受屏幕旋转导致重建等Configuration Changes
的影响。而Activity或Fragment则是UI Controller,与用户行为和ViewModel进行交互。
public class UserProfileViewModel extends ViewModel {
private String userId;
private User user;
public void init(String userId) {
this.userId = userId;
}
public User getUser() {
return user;
}
}
public class UserProfileFragment extends LifecycleFragment {
private static final String UID_KEY = "uid";
private UserProfileViewModel viewModel;
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
String userId = getArguments().getString(UID_KEY);
viewModel = ViewModelProviders.of(this).get(UserProfileViewModel.class);
viewModel.init(userId);
}
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.user_profile, container, false);
}
}
目前使用LifecycleFragment代替Fragment,等到lifecycles版本稳定后,支持库中的Fragment将实现LifecycleOwner接口。
ViewModel与Presenter最大的不同是,Presenter内部封装着一系列的行为,而ViewModel持有的是数据(状态),那么数据如何传递呢?这里就得提LiveData了。
LiveData是一个可被观察的数据持有者。它让应用中的组件观察自己的变化,却不需要显式的和刚性的依赖。LiveData同时会监听应用组件(Activity,Fragment,Services)的生命周期状态,并且做正确的事情来防止对象的内存泄露。
如果已经使用RxJava或Agera库,你可以继续使用来替代LiveData。不过当你使用它们时,确保正确处理生命周期,例如:LifecycleOwner调用了
onStop()
方法,需暂停相关的数据流;LifecycleOwner调用了onDestory()
方法,需销毁相关的数据流。你也可以添加android.arch.lifecycle:reactivestreams
工件来让LiveData和其它的响应流库(RxJava2)一起使用。
现在,用LiveData<User>
来替代User
,这样当数据改变时,Fragment将会收到通知。更好的是,LiveData是支持生命周期的,当自己不再被使用时,自动清理引用。
public class UserProfileViewModel extends ViewModel {
...
private LiveData<User> user;
public LiveData<User> getUser() {
return user;
}
}
// UserProfileFragment.java
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
...
viewModel = ViewModelProviders.of(this).get(UserProfileViewModel.class);
...
viewModel.getUser().observe(this, user -> {
// update UI
});
}
与其它使用观察回调的库不同。当Fragment不处于活跃状态时,是不会回调的,所以不需要手动在Fragment的onStop()
方法中停止观察数据;当Fragment销毁了,LiveData将会移除观察者,释放资源。
不需要做额外的事,ViewModel在配置改变时,将自动恢复相同的ViewModel实例及对当前数据的回调。
3.2.获取数据
将ViewModel和Fragment连接后,还需要ViewModel获取到用户数据。这里,假设使用Retrofit库来访问后端接口。
public interface Webservice {
/**
* @GET declares an HTTP GET request
* @Path("user") annotation on the userId parameter marks it as a
* replacement for the {user} placeholder in the @GET path
*/
@GET("/users/{user}")
Call<User> getUser(@Path("user") String userId);
}
虽然可以在ViewModel中直接获取数据并复制给User对象,但随着项目的变大,将越来越难以维护,而且给ViewModel太多的职责,与单一职责的原则相违背。此外,ViewModel的活动范围与Activity或Fragment的生命周期相关,即生命周期结束后将丢失所有数据。为此,引入了Repository这个概念。
Repository是专门用来处理数据操作的。知道从什么地方获取数据和调用什么接口更新数据,是不同数据源之间的中转,例如持久化模型、Web服务、缓存等。
public class UserRepository {
private Webservice webservice;
// ...
public LiveData<User> getUser(int userId) {
// This is not an optimal implementation, we'll fix it below
final MutableLiveData<User> data = new MutableLiveData<>();
webservice.getUser(userId).enqueue(new Callback<User>() {
@Override
public void onResponse(Call<User> call, Response<User> response) {
// error case is left out for brevity
data.setValue(response.body());
}
});
return data;
}
}
这样将数据源从应用其它部分独立出来,ViewModel不知道与什么数据源交互,便于替换。但是UserRepository需要Webservice实例,若通过构造方法提供,每个使用Webservice的类都得知道它的构造方法,使依赖变得复杂,同时都创建对象将占用大量资源。这里提供两种方法参考:
- 依赖注入:允许类定义它们之间的依赖而不需要立马构建出来,等到运行时,由其它的类提供这些依赖。
- 服务定位:提供一个仓库,类可以从中获取它们所需的依赖而不是创建它们。相较于依赖注入,它更容易实现。
这些模式可以方便地扩展代码,因为提供了清晰的方式管理依赖,而不需要增加其它代码。更重要的是它们都很容易测试。
3.3.ViewModel和Repository
public class UserProfileViewModel extends ViewModel {
private LiveData<User> user;
private UserRepository userRepo;
@Inject // UserRepository parameter is provided by Dagger 2
public UserProfileViewModel(UserRepository userRepo) {
this.userRepo = userRepo;
}
public void init(String userId) {
if (this.user != null) {
// ViewModel is created per Fragment so
// we know the userId won't change
return;
}
user = userRepo.getUser(userId);
}
public LiveData<User> getUser() {
return this.user;
}
}
3.4.缓存数据
若Repository只实现一个数据源,则会显得不太实用。需增加数据的持有,当用户再次进入界面时不用重新加载,不然会浪费宝贵的网络带宽和迫使用户等待新的请求。为此,给UserRepository添加新的数据源,在内存中缓存User对象。
@Singleton // informs Dagger that this class should be constructed once
public class UserRepository {
private Webservice webservice;
// simple in memory cache, details omitted for brevity
private UserCache userCache;
public LiveData<User> getUser(String userId) {
LiveData<User> cached = userCache.get(userId);
if (cached != null) {
return cached;
}
final MutableLiveData<User> data = new MutableLiveData<>();
userCache.put(userId, data);
// this is still suboptimal but better than before.
// a complete implementation must also handle the error cases.
webservice.getUser(userId).enqueue(new Callback<User>() {
@Override
public void onResponse(Call<User> call, Response<User> response) {
data.setValue(response.body());
}
});
return data;
}
}
3.5.持久化数据
内存缓存只能针对屏幕切换等当前应用进程存在的情况,但若进程被系统杀死,则仍需重新网络请求。为了不使用移动网络重新获取相同的数据,可以通过缓存Web请求来解决。可当场景是通过两个不同类型的请求来获取相同类型的数据时,会出现显示不一致的问题,导致需手动合并它们。正确的做法是数据持久化,在数据库中完成合并操作。
Room是一个对象映射库,通过最少的样板代码实现本地数据持久化。在编译时会验证每条查询的样式,将中断SQL查询的错误反映在编译期而不是运行时。Room封装了一些与原始SQL表和查询相关的底层实现细节,也允许观察数据库中数据(包括集合和连接查询)的变化,并通过LiveData对象来反映。此外,它还显式地定义了线程约束来规避常见问题,比如主线程上访问存储。
如果熟悉其它持久化解决方案像SQLite ORM或不同的数据库Realm,就不需要更换,除非Room的功能与用例很契合。
// 创建表相关的样式
@Entity
class User {
@PrimaryKey
private int id;
private String name;
private String lastName;
// getters and setters for fields
}
// 创建数据访问对象
@Dao
public interface UserDao {
@Insert(onConflict = REPLACE)
void save(User user);
@Query("SELECT * FROM user WHERE id = :userId")
LiveData<User> load(String userId);
}
// 创建数据库抽象类,编译自动实现
@Database(entities = {User.class}, version = 1)
public abstract class MyDatabase extends RoomDatabase {
public abstract UserDao userDao();
}
load
方法能直接返回LiveData<User>
,是很便利的。当Room发现数据库数据被改变了,且至少有一个活跃的相关的观察者,将自动通知它们更新相关操作。但是alpha1版本中,Room检查变化是基于表的修改,有可能发些无效的修改通知。
@Singleton
public class UserRepository {
private final Webservice webservice;
private final UserDao userDao;
private final Executor executor;
@Inject
public UserRepository(Webservice webservice, UserDao userDao, Executor executor) {
this.webservice = webservice;
this.userDao = userDao;
this.executor = executor;
}
public LiveData<User> getUser(String userId) {
refreshUser(userId);
// return a LiveData directly from the database.
return userDao.load(userId);
}
private void refreshUser(final String userId) {
executor.execute(() -> {
// running in a background thread
// check if user was fetched recently
boolean userExists = userDao.hasUser(FRESH_TIMEOUT);
if (!userExists) {
// refresh the data
Response response = webservice.getUser(userId).execute();
// TODO check for error etc.
// Update the database.The LiveData will automatically refresh so
// we don't need to do anything else here besides updating the database
userDao.save(response.body());
}
});
}
}
对UserRepository的改变不会影响UserProfileViewModel或者UserProfileFragment,这有利于测试,例如,提供伪造的UserRepository来测试UserProfileViewModel。
有些情况下,例如下拉刷新,通过界面向用户展示当前进行中的网络操作是很重要的。好的做法是将界面操作与实际数据分离,因为数据容易被各种原因更新(例如,获取一个朋友列表,可能会再次获取到相同的user并触发LiveData更新)。从界面的角度来看,动态请求实际上只是另一个数据点,类似其它任何数据片段(如,User对象)。这里有两种常用解决方法:
- 给
getUser
方法返回的LiveData添加网络操作的状态。 - 在Repository中提供另一个公开的方法以返回刷新的状态。尤其适合专门响应用户操作(下拉刷新),在界面中展示请求的网络状态。
需注意单一数据源原则。不同的后端接口返回相同的数据(粒度不同)是一种常见情况,但在请求的间隙,服务端的数据可能发生改变。若Repository直接返回网络请求,则导致界面显示冲突。这就是为什么在上面UserRepository实现时,网络请求的数据仅仅存到数据库中,触发LiveData的刷新。推荐,DataBase是应用唯一的数据源,而Repository是应用其它部分的唯一数据源。
3.6.测试
很容易将代码分成几个模块进行测试。
- 用户界面和交互:这是唯一使用设备界面测试的地方。最好使用Espresso测试界面代码,因为Fragment只与ViewModel交互,提供一个mock的ViewModel足够完整地测试这个界面。
- ViewModel:通过JUnit即可测试,仅mock所需的UserRepository。
- UserRepository:也可以通过JUnit测试,仅mock所需的Webservice和UserDao。主要测试是否正常调用网络服务和存储结果到数据库,以及数据被缓存或更新后没有多余的请求。由于两者都是接口,除了mock它们,还能为更复杂的测试用例模拟实现它们。
- UserDao:推荐仅使用设备测试。因为测试过程不涉及界面,仍能保持很快的运行速度。可以创建内存数据库,以确保测试没有任何副作用(如改变磁盘上的数据库文件)。同时,Room也允许指定数据库,通过提供SupportSQLiteOpenHelper的实现进行单元测试。但通常不推荐,因为手机和电脑上的SQLite版本不一样。
- Webservice:测试应该不依赖于外部,所以测试Webservice时不能通过网络访问后台。有许多库可以做到这点,例如,MockWebServer库能为你的测试创建本地网络服务。
- 测试工件:架构组件提供一个
android.arch.core:core-testing
工件来控制后台线程,包含两个JUint规则:- InstantTaskExecutorRule:此规则可强制架构组件在调用的线程上立即执行任何后台操作。
- CountingTaskExecutorRule:此规则能被用于设备测试中等待架构组件的后台操作或者连接到Espresso作为闲置资源。
3.7.最终架构
4.指导原则
- 在Manifest中定义的入口点,如:acitivy,fragment,broadcast receiver等,不是数据源。相反,它们应该只是协调与该入口点相关的数据子集。由于每个应用程序组件的存活时间很短,这取决于用户与其设备的交互以及运行时的总体状况,所以任何入口点都不应该成为数据源。
- 严格的在应用程序的各个模块之间创建明确的责任界限。例如:不要让网络加载数据的代码被多个类或包使用。同样,不要将不相关的模块(如:数据缓存和数据绑定)放到同一个类中。
- 每个模块尽可能少地暴露内部。不要尝试暴露模块内部哪怕一个地方的实现细节。你可能会在短期内节省一些时间,但是随着项目的发展,你将会花更多的时间来偿还。
- 当定义模块间的交互时,考虑如何让每个模块能独立的测试。例如,拥有一个定义良好的、从网络获取数据的接口模块,将会使持久化数据到本地数据库的行为更易于测试。相反,如果将两个模块的逻辑放在一个地方,或者将网络相关的代码扩散到整个项目,测试将会变的非常困难(并非不可能)。
- 应用程序的核心才是重点。不要花费时间重复造轮子或一次又一次的编写相同的样板代码。相反,将精力集中在应用程序的核心上,让AAC和其它优秀的库来处理重复的样板代码。
- 持久化尽可能多的相关最新数据,以便应用程序在设备处于离线模式时还可以使用。即使你可以享用稳定高速的网络连接,但是你的用户可能无法享用。
- Repository应该指定单一的数据源。每当应用程序需要访问数据时,数据应该始终来源于一个地方。
5.暴露网络状态
对于上面提到的网络状态问题,可以使用Resource类封装数据和状态。
//a generic class that describes a data with a status
public class Resource<T> {
@NonNull public final Status status;
@Nullable public final T data;
@Nullable public final String message;
private Resource(@NonNull Status status, @Nullable T data, @Nullable String message) {
this.status = status;
this.data = data;
this.message = message;
}
public static <T> Resource<T> success(@NonNull T data) {
return new Resource<>(SUCCESS, data, null);
}
public static <T> Resource<T> error(String msg, @Nullable T data) {
return new Resource<>(ERROR, data, msg);
}
public static <T> Resource<T> loading(@Nullable T data) {
return new Resource<>(LOADING, data, null);
}
}
从网络获取数据再将磁盘存储的数据展示出来,是一种常见的现象,对于重复的逻辑(见下图)可以提取出NetworkBoundResource类来复用。
首先观察数据库的资源响应数据的更新。当从数据库中获取时,先判断得到的结果是否符合使用要求,不然就从网络获取。若想从网络更新时显示存储数据,它们可以同时进行。若网络加载成功,将结果存到数据库中,再次触发加载流程;若加载失败,直接调用失败操作。
新的数据存到磁盘上后,需从数据库重新取数据,若数据库可以发送改变消息,通常不用这么做。但是,依赖数据库发送改变消息,也会有些问题,因为数据更新后可能并没有变化,数据库将不会发送改变信息。同样,也不希望直接使用网络请求到的结果,因为这不符合单一数据源原则,哪怕也会触发数据库存储更新。最后,不希望没新数据更新的情况下发送SUCCESS标志,这会给客户端错误的信息。
// ResultType: Type for the Resource data
// RequestType: Type for the API response
public abstract class NetworkBoundResource<ResultType, RequestType> {
// Called to save the result of the API response into the database
@WorkerThread
protected abstract void saveCallResult(@NonNull RequestType item);
// Called with the data in the database to decide whether it should be
// fetched from the network.
@MainThread
protected abstract boolean shouldFetch(@Nullable ResultType data);
// Called to get the cached data from the database
@NonNull @MainThread
protected abstract LiveData<ResultType> loadFromDb();
// Called to create the API call.
@NonNull @MainThread
protected abstract LiveData<ApiResponse<RequestType>> createCall();
// Called when the fetch fails. The child class may want to reset components
// like rate limiter.
@MainThread
protected void onFetchFailed() {
}
// returns a LiveData that represents the resource
public final LiveData<Resource<ResultType>> getAsLiveData() {
return result;
}
}
上面的类定义两种类型参数(ResultType,RequestType),是考虑到网络获取的数据类型与本地使用的数据类型不匹配。而使用ApiResponse作为网络请求,是因为它对
Retrofit2.Call
进行简单的封装,将返回类型转成LiveData。
对于上面提出的要求,可以通过以下具体实现:
public abstract class NetworkBoundResource<ResultType, RequestType> {
private final MediatorLiveData<Resource<ResultType>> result = new MediatorLiveData<>();
@MainThread
NetworkBoundResource() {
result.setValue(Resource.loading(null));
LiveData<ResultType> dbSource = loadFromDb();
result.addSource(dbSource, data -> {
result.removeSource(dbSource);
if (shouldFetch(data)) {
fetchFromNetwork(dbSource);
} else {
result.addSource(dbSource,
newData -> result.setValue(Resource.success(newData)));
}
});
}
private void fetchFromNetwork(final LiveData<ResultType> dbSource) {
LiveData<ApiResponse<RequestType>> apiResponse = createCall();
// we re-attach dbSource as a new source,
// it will dispatch its latest value quickly
result.addSource(dbSource,
newData -> result.setValue(Resource.loading(newData)));
result.addSource(apiResponse, response -> {
result.removeSource(apiResponse);
result.removeSource(dbSource);
//noinspection ConstantConditions
if (response.isSuccessful()) {
saveResultAndReInit(response);
} else {
onFetchFailed();
result.addSource(dbSource,
newData -> result.setValue(
Resource.error(response.errorMessage, newData)));
}
});
}
@MainThread
private void saveResultAndReInit(ApiResponse<RequestType> response) {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... voids) {
saveCallResult(response.body);
return null;
}
@Override
protected void onPostExecute(Void aVoid) {
// we specially request a new live data,
// otherwise we will get immediately last cached value,
// which may not be updated with latest results received from network.
result.addSource(loadFromDb(),
newData -> result.setValue(Resource.success(newData)));
}
}.execute();
}
}
现在,用NetworkBoundResource替换之前UserRepository的获取数据的操作。由于Webservice和UserDao是由项目决定类型,无法提取到NetworkBoundResource中,所以相关操作放在UserRepository中实现。
class UserRepository {
Webservice webservice;
UserDao userDao;
public LiveData<Resource<User>> loadUser(final String userId) {
return new NetworkBoundResource<User,User>() {
@Override
protected void saveCallResult(@NonNull User item) {
userDao.insert(item);
}
@Override
protected boolean shouldFetch(@Nullable User data) {
return rateLimiter.canFetch(userId) && (data == null || !isFresh(data));
}
@NonNull @Override
protected LiveData<User> loadFromDb() {
return userDao.load(userId);
}
@NonNull @Override
protected LiveData<ApiResponse<User>> createCall() {
return webservice.getUser(userId);
}
}.getAsLiveData();
}
}