private LiveData user;
public LiveData getUser() {
return user;
}
}
然后将UserProfileFragment修改如下,观察数据并更新UI:
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
viewModel.getUser().observe(this, user -> {
// update UI
});
}
一旦用户数据更新,onChanged回调将被调用然后UI会被刷新。
如果你熟悉一些使用观察者模式第三方库,你会觉得奇怪,为什么没有在Fragment的onStop()方法中将观察者移除。对于LiveData来说这是没有必要的,因为它是生命周期感知的,这意味着如果UI处于不活动状态,它就不会调用观察者的回调来更新数据。并且在onDestroy后会自动移除。
我们也不需要处理任何视图重建(如屏幕旋转)。ViewModel会自动恢复重建前的数据。当新的视图被创建出来后,它会接收到与之前相同的ViewModel实例,并且观察者的回调会被立刻调用,更新最新的数据。这也是ViewModel为什么不能直接引用视图对象,因为它的生命周期长于视图对象。
现在我们将视图和模型连接起来,但是模型该怎么获取数据呢?在这个例子中,我们假设使用REST API从后台获取。我们将使用Retrofit来向后台请求数据。
我们的retrofit类Webservice如下:
public interface Webservice {
/**
如果只是简单的实现,ViewModel可以直接操作Webservice来获取用户数据。虽然这样可以正常工作,但你的应用无法保证它的后续迭代。因为这样做将太多的责任让ViewModel来承担,这样就违反类之前讲到的分层原则。又因为ViewModel的生命周期是绑定在Activity和Fragment上的,所以当UI被销毁后如果丢失所有数据将是很差的用户体验。所以我们的ViewModel将和一个新的模块进行交互,这个模块叫Repository。
Repository模块负责处理数据。它为应用程序的其余部分提供了一个干净的API。他知道在数据更新时从哪里获取数据和调用哪些API调用。你可以将它们视为不同数据源(持久性模型,Web服务,缓存等)之间的中介者。
UserRepository类如下:
public class UserRepository {
private Webservice webservice;
// …
public LiveData getUser(int userId) {
// This is not an optimal implementation, we’ll fix it below
final MutableLiveData data = new MutableLiveData<>();
webservice.getUser(userId).enqueue(new Callback() {
@Override
public void onResponse(Call call, Response response) {
// error case is left out for brevity
data.setValue(response.body());
}
});
return data;
}
}
虽然repository模块看上去没有必要,但他起着重要的作用。它为App的其他部分抽象出了数据源。现在我们的ViewModel并不知道数据是通过WebService来获取的,这意味着我们可以随意替换掉获取数据的实现。
上面这种写法可以看出来UserRepository需要初始化Webservice实例,这虽然说起来简单,但要实现的话还需要知道Webservice的具体构造方法该如何写。这将加大代码的复杂度,另外UserRepository可能并不是唯一使用Webservice的对象,所以这种在内部构建Webservice实例显然是不推荐的,下面有两种模式来解决这个问题:
这些模式允许你扩展代码,因为它们提供明确的模式来管理依赖关系,而不会重复代码或增加复杂性。两者都允许交换实现进行测试;这是使用它们的主要好处之一。在这个例子中,我们将使用Dagger 2来管理依赖关系。
现在,我们的UserProfileViewModel可以改写成这样:
public class UserProfileViewModel extends ViewModel {
private LiveData 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 getUser() {
return this.user;
}
}
上面的Repository虽然网络请求做了封装,但是它依赖后台数据源,所以存在不足。
上面的UserRepository实现的问题是,在获取数据之后,它不会保留在任何地方。如果用户离开UserProfileFragment并重新进来,则应用程序将重新获取数据。这是不好的,有两个原因:它浪费了宝贵的网络带宽和迫使用户等待新的查询完成。为了解决这个问题,我们将向我们的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 getUser(String userId) {
LiveData cached = userCache.get(userId);
if (cached != null) {
return cached;
}
final MutableLiveData 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() {
@Override
public void onResponse(Call call, Response response) {
data.setValue(response.body());
}
});
return data;
}
}
在当前的实现中,如果用户旋转屏幕或离开并返回到应用程序,现有UI将立即可见,因为Repository会从内存中检索数据。但是,如果用户离开应用程序,并在Android操作系统杀死进程后几小时后又会怎么样?
在目前的实现中,我们将需要从网络中再次获取数据。这不仅是一个糟糕的用户体验,也是浪费,因为它将使用移动数据来重新获取相同的数据。你以通过缓存Web请求来简单地解决这个问题,但它会产生新的问题。如果请求一个朋友列表而不是单个用户,会发生什么情况?那么你的应用程序可能会显示不一致的数据,这是最令人困惑的用户体验。例如,相同的用户的数据可能会不同,因为朋友列表请求和用户请求可以在不同的时间执行。你的应用需要合并他们,以避免显示不一致的数据。
正确的处理方法是使用持久模型。这时候Room就派上用场了。
Room是一个对象映射库,它提供本地数据持久性和最少的样板代码。在编译时,它根据模式验证每个查询,从而错误的SQL查询会导致编译时错误,而不是运行时失败。Room抽象了使用原始SQL表和查询的一些基本实现细节。它还允许观察数据库数据(包括集合和连接查询)的更改,通过LiveData对象公开这些更改。
要使用Room我们首先需要使用@Entity来定义实体:
@Entity
class User {
@PrimaryKey
private int id;
private String name;
private String lastName;
// getters and setters for fields
}
接着创建数据库类:
@Database(entities = {User.class}, version = 1)
public abstract class MyDatabase extends RoomDatabase {
}
值得注意的是MyDatabase是一个抽象了,Room会在编译期间提供它的一个实现类。
接下来需要定义DAO:
@Dao
public interface UserDao {
@Insert(onConflict = REPLACE)
void save(User user);
@Query(“SELECT * FROM user WHERE id = :userId”)
LiveData load(String userId);
}
接着在MyDatabase中添加获取上面这个DAO的方法:
@Database(entities = {User.class}, version = 1)
public abstract class MyDatabase extends RoomDatabase {
public abstract UserDao userDao();
}
这里的load方法返回的是LiveData,所以当相关数据库中的数据有任何变化时,Room都会通知LiveData上的处于活动状态的观察者。
现在我们可以修改UserRepository了:
@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 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的直接数据来源从Webservice改为本地数据库,但我们却不需要修改UserProfileViewModel或者UserProfileFragment。这就是抽象层带来的好处。这也给测试带来了方便,因为你可以提供一个虚假的UserRepository来测试你的UserProfileViewModel。
现在,如果用户重新回到这个界面,他们会立刻看到数据,因为我们已经将数据做了持久化的保存。当然如果有用例需要,我们也可不展示太老旧的持久化数据。
在一些用例中,比如下拉刷新,如果正处于网络请求中,那UI需要告诉用户正处于网络请求中。一个好的实践方式就是将UI与数据分离,因为UI可能因为各种原因被更新。从UI的角度来说,请求中的数据和本地数据类似,只是它还没有被持久化到数据库中。
以下有两种解决方法:
在以上实例中,数据唯一来源是数据库,这样做的好处是用户可以基于稳定的数据库数据来更新页面,而不需要处理大量的网络请求状态。数据库有数据则使用,没有数据则等待其更新。
我们之前提到分层可以个应用提供良好的测试能力,接下来就看看我们怎么测试不同的模块。
Android UI Instrumentation test
的测试模块。测试UI的最好方法就是使用Espresso框架。你可以创建Fragment然后提供一个虚假的ViewModel。因为Fragment只跟ViewModel交互,所以虚拟一个ViewModel就足够了。[图片上传失败…(image-7b4a65-1601127755214)]
编程是一个创意领域,构建Android应用程序也不例外。有多种方法来解决问题,无论是在多个Activity或Fragment之间传递数据,还是检索远程数据并将其在本地保持离线模式,或者是任何其他常见的场景。
虽然以下建议不是强制性的,但经验告诉我们,遵循这些建议将使你的代码库从长远来看更加强大,可测试和可维护。
在上面的小结我们故意省略了网络错误和加载状态来保证例子的简洁性。在这一小结我们演示一种使用Resource类来封装数据及其状态。以此来公开网络状态。
下面是简单的Resource实现:
//a generic class that describes a data with a status
public class Resource {
@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 Resource success(@NonNull T data) {
return new Resource<>(SUCCESS, data, null);
}
public static Resource error(String msg, @Nullable T data) {
return new Resource<>(ERROR, data, msg);
}
public static Resource loading(@Nullable T data) {
return new Resource<>(LOADING, data, null);
}
}
以为从网络上抓取视频的同时在UI上显示数据库的旧数据是很常见的用例,所以我们要创建一个可以在多个地方重复使用的帮助类NetworkBoundResource。以下是NetworkBoundResource的决策树:
[图片上传失败…(image-736553-1601127755211)]
NetworkBoundResource从观察数据库开始,当第一次从数据库加载完实体后,NetworkBoundResource会检查这个结果是否满足用来展示的需求,如不满足则需要从网上重新获取。当然以上两种情况可能同时发生,你希望先将数据显示在UI上的同时去网络上请求新数据。
如果网络请求成果,则将结果保存到数据库,然后重新从数据库加载数据,如果网络请求失败,则直接传递错误信息。
**注意:**在上面的过程中可以看到当将新数据保存到数据库后,我们重新从数据库加载数据。虽然大部分情况我们不必如此,因为数据库会为我们传递此次更新。但另一方面,依赖数据库内部的更新机制并不是我们想要的如果更新的数据与旧数据一致,则数据谷不会做出更新提示。我们也不希望直接从网络请求中获取数据直接用于UI,因为这样违背了单一数据源的原则。
下面是NetworkBoundResource类的公共api:
// ResultType: Type for the Resource data
// RequestType: Type for the API response
public abstract class NetworkBoundResource
// 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 loadFromDb();
// Called to create the API call.
@NonNull @MainThread
protected abstract LiveData
// 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
return result;
}
}
注意到上面定义了两种泛型,ResultType和RequestType,因为从网络请求返回的数据类型可能会和数据库返回的不一致。
eCall();
// 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
return result;
}
}
注意到上面定义了两种泛型,ResultType和RequestType,因为从网络请求返回的数据类型可能会和数据库返回的不一致。