本指南使用于具备构建应用程序基础而且想了解构建强大、优质的应用程序的最佳实践和推荐架构的开发人员。html
注:本指南假定读者熟悉 Android Framework。若是你是一个应用程序开发的新手,请参阅入门指南系列培训,其中包含了本指南先决条件的相关主题。java
在大多数状况下,桌面应用程序在启动器快捷方式中有一个单一的入口而且做为单独的独立进程运行,与桌面应用程序不一样,Android 应用具备更复杂的结构。一个典型的 Android 应用是由多个应用程序组件构成的,包括 activity,fragment,service,content provider 和 broadcast receiver。react
这些应用程序组件中的大部分声明在由 Android OS 使用的应用程序清单中,用来决定如何将应用融入到用户设备的总体体验中。尽管如前所述,传统的桌面应用程序做为独立进程运行,可是正确的编写 Android 应用程序须要更加灵活,由于用户会同过设备上不一样的应用程序组织成本身的方式不断切换流程和任务。android
例如,考虑下在你喜欢的社交网络应用中分享照片时会发生什么。该应用会触发一个启动相机的 intent,从该 intent 中 Android OS 会启动一个相机应用来处理这个请求。在此刻,用户离开社交网络应用可是用户的体验是无缝的。相机应用转而可能会触发其它的 intent,例如启动文件选择器,这可能会启动另外一个应用。最终用户回到社交网络应用而且分享照片。此外,在这个过程当中的任什么时候刻用户都有可能会被一个电话打断,而且在结束通话后再回来继续分享照片。git
在 Android 中,这种应用切换行为很常见,因此你的应用程序必须正确处理这些流程。记住,移动设备的资源是有限的,因此在任什么时候候,操做系统均可能会杀死一些应用为新的应用腾出空间。github
其中的重点是应用程序组件可能会被单独和无序的启动,而且可能会被用户或系统在任什么时候候销毁。由于应用程序组件是短暂的,而且其声明周期(何时被建立和销毁)不受你控制,因此不该该在应用程序组件中存储任何应用数据或状态,同时应用程序组件不该该相互依赖。web
若是不能在应用程序组件中存储应用数据和状态,那么应该如何构建应用?数据库
最重要的是在应用中要专一于关注点分离。一个常见的错误是在 Activity 或 Fragment 中编写全部的代码。任何不是处理 UI 或 操做系统交互的代码都不该该在这些类中。保持它们尽量的精简能够避免许多与生命周期有关的问题。不要忘记你不拥有这些类,它们只是体现了 OS 和 应用之间协议的粘合类。Android OS 可能会由于用户交互或其余因素(如低内存)的缘由在任什么时候候销毁它们。最好尽可能减小对它们的依赖以提供一个稳固的用户体验。编程
第二个重要的原则是应该用 Model 驱动 UI,最好是持久化的 Model。持久化是最佳的缘由有两个:一是若是 OS 销毁应用释放资源,用户不用担忧丢失数据;二是即便网络链接不可靠或者是断开的,应用仍将继续运行。Model 是负责处理应用数据的组件。Modle 独立于应用中的 View 和应用程序组件,所以 Model 和这些组件的生命周期问题隔离开了。保持 UI 代码精简而且摒除应用的逻辑使其更易于管理。基于 Model 类构建的应用程序其管理数据的职责明确,使应用程序可测试而且稳定。后端
在本节中,咱们将经过一个用例来演示如何使用 Architecture Components 构建应用程序。
注:不可能有一种应用程序的编写方式对于每种状况都是最好的。话虽如此,这个推荐的架构应该是大多数用例的良好起点。若是你已经有一种很好的应用程序编写方式则不须要改变。
假设咱们正在构建一个显示用户我的信息的 UI。用户的我的信息将使用 REST API 从咱们本身的私有后端获取。
UI 包含一个 fragment 文件 UserProfileFragment.java 和其布局文件 user_profile_layout.xml。
为了驱动 UI,数据模型须要持有两个数据元素。
用户 ID :用户的标识符。最好使用 fragment 的参数将用户 ID 传到 fragment 中。若是 Android OS 销毁进程,该 ID 将会被保存,以便下次应用重启时该 ID 可用。
用户对象:保存用户数据的普通 Java 对象(POJO)。
咱们将会基于 ViewModel 来建立一个 UserProfileViewModel 来保存这些信息。
ViewModel 为指定的 UI 组件(如:fragment 或 activity)提供数据,而且负责与数据处理的业务部分的交互,例如:调用其它组件获取数据或转发用户的操做。ViewModel 对于 View 并不了解而且不受配置改变(如:因为旋转致使 activity 的从新建立)的影响
如今有 3 个文件
user_profile.xml:屏幕上的 UI 定义。
UserProfileViewModel.java:为 UI 准备数据的类。
UserProfileFragment.java:用于在 ViewModel 中显示数据并对用户交互作出反应的 UI 控制器。
下面是咱们的初始实现(简单起见省略布局文件):
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 类。在 Architecture Components 中的生命周期 API 稳定后, Android 支持包中的 Fragment 类将会实现 LifecycleOwner 接口。
如今咱们有了 3 个代码模块,怎样链接它们?最后,当 ViewModel 的用户字段被设置时,须要一种方式来通知 UI。这正是 LiveData 的用武之地。
LiveData 是一个可观察的数据持有者。它容许应用程序中的组件观察 LiveData 进行改变,而不会在组件之间建立显示的,固定的依赖。另外,LiveData 还遵照应用程序组件(如:activity,fragment,service)的生命周期状态,而且防止对象泄漏使应用不会消耗更多的内存。
注:若是你已经再使用像 RxJava 或 Agera 的库,你能够继续使用它们而不用换成 LiveData。可是当使用它们或其它的方式时,请确保正确处理生命周期,如:当相关 LifecycleOwner 中止时暂停数据流或在 LifecycleOwner 被销毁时销毁数据流。能够添加 android.arch.lifecycle:reactivestreams 工具,和其它的响应流库(如:RxJava2)一块儿使用 LiveData。
将 UserProfileViewModel 中的 User 字段替换为 LiveData
public class UserProfileViewModel extends ViewModel {
...
// private User user;
private LiveData<User> user;
public LiveData<User> getUser() {
return user;
}
}复制代码
修改 UserprofileFragment 来观察数据并更新 UI。
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
viewModel.getUser().observe(this, user -> {
// 更新 UI
});
}复制代码
每次更新用户数据时,将会调用 onChange 回调而且更新 UI。
若是你熟悉其它库的可观察回调的使用,可能已经意识到咱们没有重写 fragment 的 onStop() 方法来中止观察数据。这对于 LiveData 来讲是没必要要的,由于 LiveData 是证实周期感知的,这意味着除非 fragment 处于活动状态(收到了 onStart() 但尚未收到 onStop()),不然它不会调用回调。当 fragment 收到 onDestroy() 时 LiveData 会自动移除观察者。
咱们没有作任何事情来特别是处理配置的变化(如:用户旋转屏幕)。当配置发生变化时 ViewModel 将会自动恢复,因此,只要新的 fragment 启动,它将会收到属于 ViewModel 的相同实例,而且使用最新的数据当即调用回调。这就是为何 ViewModel 不该该直接引用 View,ViewModel 可能存活的比 View 的生命周期长。请参阅 ViewModel 的生命周期。
咱们已经将 ViewModel 连接到了 fragment,可是 ViewModel 怎样获取数据呢?这个例子中,假设咱们的后端提供了一个 REST API。咱们将会使用 Retrofit 库来访问后端,你也能够自由的使用其它库来达到一样的目的。
这是 retrofit 的 Webservice 类,用于和后端通信:
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 实现能够直接调用 Webservice 获取数据并将其分配给用户对象。即便这样可使用,可是应用程序将会随着增加而难以维护。将太多的职责交给 ViewModel 这违反了咱们前面提到的关注点分离的原则。此外,ViewModel 的做用域依赖于 Activity 或 Fragment 的生命周期,所以在 Activity 或 Fragment 的生命周期结束时丢失全部的数据是一种很差的用户体验。故而,咱们的 ViewModel 将把这项工做委托给一个新的 Repository 模块。
Repository 模块负责处理数据操做。它们为应用程序的其它部分提供了一个干净的 API。它们知道在数据更新时从哪里获取数据和调用哪些 API。能够将其视为不一样数据源(持久化模型,Web 服务,缓存等)之间的中间层。
下面的 UserRepository 类使用 WebService 来获取用户数据。
public class UserRepository {
private Webservice webservice;
// ...
public LiveData<User> getUser(int userId) {
// 这是最佳的实现,下面会有解释
final MutableLiveData<User> data = new MutableLiveData<>();
webservice.getUser(userId).enqueue(new Callback<User>() {
@Override
public void onResponse(Call<User> call, Response<User> response) {
// 为了简单起见省略错误的状况
data.setValue(response.body());
}
});
return data;
}
}复制代码
虽然 Repository 模块看起来是没必要要的,可是它起着一个重要的做用;它抽象了应用程序其它部分的数据源。如今 ViewModel 不知道数据是由 Webservice 获取的,这意味着能够根据需求将其切换为其它实现。
注:为了简单起见,咱们忽略了网络错误的状况。有关于暴露错误和加载状态的可选实现方式,请参阅附录:暴露网络状态。
上面的 UserRepository 类须要一个 Webservice 的实例来完成其工做。能够简单的建立 Webservice,可是这须要知道 Webservice 的依赖来构造它。这将会显著的使代码复杂和重复(例如:须要 Webservice 实例的每一个类都须要知道如何使用它的依赖来构造它)。另外,UserRepostory 可能不是惟一须要 Webservice 的类。若是每一个类都建立一个新的 Webservice,这将会形成很是大的资源负担。
有两种模式能够解决这个问题:
依赖注入:依赖注入容许类定义其依赖而不构造它们。在运行时,另外一个类负责提供这些依赖。推荐使用 Google 的 Dagger 2 库在 Android 应用中实现依赖注入。Dagger 2 经过遍历依赖关系树自动构建对象并为依赖提供编译时保障。
服务定位:服务定位提供了一个注册表,类能够从中获取它们的依赖关系,而不是构造它们。与依赖注入(DI)相比,服务定位实现起来相对容易,因此若是不熟悉 DI,请使用服务定位代替。
这些模式容许你扩展代码,由于它们提供清晰的模式来管理依赖关系,而不会重复代码或增长复杂性。二者都容许替换实现进行测试;这是使用它们的主要好处之一。
在这个例子中,咱们将使用 Dagger 2 来管理依赖。
修改 UserProfileViewModel 以使用 Repository。
public class UserProfileViewModel extends ViewModel {
private LiveData<User> user;
private UserRepository userRepo;
@Inject // UserRepository 参数由 Dagger 2 提供
public UserProfileViewModel(UserRepository userRepo) {
this.userRepo = userRepo;
}
public void init(String userId) {
if (this.user != null) {
// ViewModel 是由 Fragment 建立的
// 因此咱们知道 userId 不会改变
return;
}
user = userRepo.getUser(userId);
}
public LiveData<User> getUser() {
return this.user;
}
}复制代码
上述 Repository 的实现对于抽象调用 Web 服务是很好的,可是由于它仅依赖于一个数据源因此不是很实用。
上述 UserRepository 的实现的问题是在获取到数据以后没有把数据保存下来。若是用户离开 UserProfileFragment 而后返回回来,应用将会从新获取数据。这样是很很差的,缘由有两个:一是浪费了宝贵的网络带宽;二是迫使用户等待新的查询完成。为了解决这个问题,咱们将在 UserRepository 中添加一个新的数据源,用以在内存中缓存 User 对象。
@Singleton // 告诉 Dagger 这个类只应该构造一次
public class UserRepository {
private Webservice webservice;
// 简单的内存缓存,为了简单忽略相关细节
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);
// 这还不是最好的但比之前的好。
// 一个完整的实现必须处理错误的状况。
webservice.getUser(userId).enqueue(new Callback<User>() {
@Override
public void onResponse(Call<User> call, Response<User> response) {
data.setValue(response.body());
}
});
return data;
}
}复制代码
在当前的实现中,若是用户旋转屏幕或离开并返回应用,已存在的 UI 将会当即可见,由于 Repository 从内存缓存中取回数据。可是,若是用户离开应用并在几个小时后(Android OS 已经杀死进程后)返回又会发生什么?
若是是目前的实现,将会须要再次从网络获取数据。这不只是一个很差的用户体验,而且也是浪费,由于这将会使用移动数据从新获取一样的数据。能够经过缓存 Web 请求来简单的解决这个问题,可是这会致使新的问题。若是相同的用户数据来自另外一种请求并显示(例如:获取朋友列表)将会怎样?这时应用会显示不一致的数据,这是最使人困惑的用户体验。例如:相同用户的数据可能会显示的不一样,由于朋友列表的请求和用户我的信息的请求可能会在不一样的时间执行。应用程序须要合并它们以免显示不一致的数据。
解决这个问题的正确方法是使用持久化模型。这就是持久化库 Room 的用武之地了。
Room 是一个以最少的样板代码提供本地数据持久化的对象映射库。在编译时,它会根据模式验证每一个查询,因此损坏的 SQL 查询只会致使编译时错误,而不是运行时崩溃。Room 抽象出一些使用原始 SQL 表查询的底层实现细节。它还容许观察数据库数据(包括集合和链接查询)的变化,并经过 LiveData 对象暴露这些变化。另外,它明肯定义了线程约束以解决常见问题(如在主线程访问存储)。
注:若是你熟悉其它的持久化解决方案,如:SQLite ORM 或像 Realm 等其余的数据库,你不须要用更换为 Room,除非 Room 的功能对你的用例更加适用。
使用 Room,须要定义咱们的局部模式。首先,用 @Entity 注释 User 类,将其标记为数据库中的一个表。
@Entity
class User {
@PrimaryKey
private int id;
private String name;
private String lastName;
// 字段的 get 和 set 方法
}复制代码
而后,经过继承 RoomDatabase 为应用建立一个数据库。
@Database(entities = {User.class}, version = 1)
public abstract class MyDatabase extends RoomDatabase {
}复制代码
请注意,MyDatabase 是抽象类,Room 会自动提供其实现类。详细信息请参阅 Room 的文档。
如今须要一种方法来将用户数据插入数据库。为此,咱们将建立一个数据库访问对象( DAO )。
@Dao
public interface UserDao {
@Insert(onConflict = REPLACE)
void save(User user);
@Query("SELECT * FROM user WHERE id = :userId")
LiveData<User> load(String userId);
}复制代码
而后,在数据库类中引用 DAO。
@Database(entities = {User.class}, version = 1)
public abstract class MyDatabase extends RoomDatabase {
public abstract UserDao userDao();
}复制代码
请注意,load 方法的返回值是 LiveData
注:从 alpha 1 版本开始,Room 基于表修改的检查无效,这意味着它可能会发送错误的通知。
如今修改 UserRepository 来整合 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);
// 直接从数据库返回一个 LiveData。
return userDao.load(userId);
}
private void refreshUser(final String userId) {
executor.execute(() -> {
// 在后台线程中运行
// 检查最近是否获取过 user
boolean userExists = userDao.hasUser(FRESH_TIMEOUT);
if (!userExists) {
// 刷新数据
Response response = webservice.getUser(userId).execute();
// TODO 检查错误等。
// 更新数据库。LiveData 将会自动刷新
// 因此除了更新数据库外不须要任何操做。
userDao.save(response.body());
}
});
}
}复制代码
请注意,即便咱们更改了 UserRepository 中的数据来源,咱们也不须要更改 UserProfileViewModel 或 UserProfileFragment。这是抽象带来的灵活性。这样也很是易于测试,由于在测试 UserProfileViewModel 能够提供一个假的 UserRepository。
如今咱们的代码是完整的,若是用户往后再回到相同的 UI,他们会当即看到用户信息,由于咱们已经将其持久化了。同时,若是数据过时,Repository 将会在后台更新数据。固然,根据你的用例,若是持久化的数据太旧你可能不但愿显示它们。
在一些用例中,以下拉刷新,在进行网络操做时显示用户数据对于 UI 来讲很是重要。将 UI 操做从实际数据中分离是一个很好的作法,由于其可能因为各类缘由而被更新(例如:若是获取一个朋友列表,可能会再次获取到相同的 user 并触发 LiveData
这种用例有两种常见的解决方案:
更改 getUser 以返回包含网络操做状态的 LiveData。在附录:暴露网络状态部分提供了一个实现例子。
在 Repository 类中提供一个能够返回 User 刷新状态的公共方法。若是只是为了响应明确的用户操做而在 UI 中显示网络状态(如:下拉刷新),则这种方式是更好的选择。
不一样的 REST API 接口返回相同的数据是很常见的。例如,若是后端有另外一个返回朋友列表的接口,相同的用户对象(也许是不一样的粒度)可能来自两个不一样的 API 接口。若是 UserRepository 把从 Webservice 请求获取到的响应原样返回,码么 UI 可能会显示不一致的数据,由于在这些请求之间数据可能在服务端发生了改变。这就是为何在 UserRepository 的实现中 Web 服务的回调只是将数据保存到了数据库。而后,对数据库的更改将会触发处于活动状态的 LiveData 对象上的回调。
在这个模型中,数据库服务做为单一数据源,应用程序的其它部分经过 Repository 来访问它。不管是不是否磁盘缓存,建议 Repository 指定一个数据源做为应用程序其它部分的单一数据源。
咱们已经提到分离的好处之一是可测试性。让咱们看看如何测试每一个代码模块。
用户界面或用户交互:这将是惟一一次须要 Android UI Instrumentation test。测试 UI 代码的最佳方式是建立一个 Espresso 测试。能够建立一个 fragment 并为其提供一个模拟的 ViewModel。由于 fragment 只和 ViewModel 交互,随意模拟 ViewModel 足以彻底测是 UI。
ViewModel:可使用 JUnit test 来测试 ViewModel。只须要模拟 UserRepository 来测试它。
UserRepository:也可使用 JUnit test 来测试 UserRepository。须要模拟 Webservice 和 DAO。能够测试 UserRepository 是否进行了正确的 Web 服务调用,将结构保存到数据库,若是数据被缓存且是最新的,则不会发起任何没必要要的请求。由于 WebService 和 UserDao 都是接口,因此能够模拟它们或者为更复杂的测试用例建立假的实现。
UserDao:推荐使用 Instrumentation 测试的方式测试 DAO 类。由于 Instrumentation 测试不须要任何 UI,它们会运行的很快。对于每一个测试,能够建立一个内存数据库以确保测试没有任何反作用(例如:改变磁盘上的数据库文件)。
Room 还容许指定数据库实现,因此能够经过向其提供 SupportSQLiteOpenHelper 的 JUnit 实现来测试它。一般不推荐这种方式,由于设备上运行的 SQLite 版本可能和主机上的 SQLite 版本不一样。
Webservice:重点是使测试相对于外部独立,因此 Webservice 的测试要避免经过网络调用后端。有许多库能够帮助完成该测试。例如:MockWebServer 是一个很好的库,能够帮助为测试建立一个假的本地服务。
测试工件架构组件提供了一个 maven 工件来控制其后台线程。在 android.arch.core:core-testing 工件中,有两个 JUnit 规则:
InstantTaskExecutorRule:该规则可用于强制架构组件当即执行调用线程上的任何后台操做。
CountingTaskExecutorRule:该规则可用于 Instrumentation 测试,以等待架构组件的后台操做,或将其链接到 Espresso 做为闲置资源。
下图显示了推荐架构中的全部模块以及它们如何互相交互:
编程是一个创做领域,构建 Android 应用也不例外。有许多方法来解决问题,不管是在多个 activity 或 fragment 之间传递数据,是获取远程数据并为了离线模式将其持久化到本地,仍是特殊应用遭遇的其它常见状况。
虽然一下建议不是强制性的,可是以咱们的经验,从长远来看,遵循这些建议将会使代码库更健壮,易测试和易维护。
在 manifest 中定义的入口点,如:acitivy,fragment,broadcast receiver 等,不是数据源。相反,它们应该只是协调与该入口点相关的数据子集。因为每一个应用程序组件的存活时间很短,这取决于用户与其设备的交互以及运行时的整体情况,因此任何入口点都不该该成为数据源。
严格的在应用程序的各个模块之间建立明确的责任界限。例如:不在代码库中的多个类或包中扩散从网络加载数据的代码。一样,不要将无关的责任(如:数据缓存和数据绑定)放到同一个类中。
每一个模块尽量少的暴露出来。不要视图建立暴露模块内部实现细节的“只一个”的快捷方式。你可能会在短时间内节省一些时间,可是随着代码库的发展,你将会屡次偿还更多的基数债务。
当定义模块间的交互时,请考虑如何让每一个模块能够独立的测试。例如,拥有一个用于从网络获取数据且定义良好的 API 的模块,将会使其更易于测试在本地数据库中持久化数据。相反,若是将两个模块的逻辑放在一个地方,或者将网络代码扩散到整个代码库,测试将会变的很是困难(并不是不可能)。
应用程序的核心是使其脱颖而出。不要花费时间重复造轮子或一次又一次的编写相同的样板代码。相反,将精力集中在使应用程序独一无二上,让 Android Architecture Components 和其它的优秀的库来处理重复的样板代码。
持久化尽量多的相关最新数据,以便应用程序在设备处于离线模式时还可使用。即便你能够享用稳定高速的网络链接,可是你的用户可能没法享用。
Repository 应该指定一个数据源做为单一数据源。每当应用程序须要访问数据时,数据应该始终来源于单一数据源。有关更多信息,请参阅单一数据源
在上面推荐的应用程序架构部分,为了保持示例简单咱们故意忽略网络错误和加载状态。在本节中,咱们演示一种经过 Resource 类暴露网络状态来封装数据和其状态。
如下是一个实现的例子:
// 描述数据和其状态的类
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。下面是 NetworkBoundResource 的决策树。
它经过观察资源的数据库。当首次从数据库加载条目时,NetworkBoundResource 检查返回结果是否足够好能够被发送和(或)应该从网络获取数据。请注意,他们可能同时发生,由于你可能会但愿在显示缓存数据的同时从网络更新数据。
若是网络调用成功,则将返回数据保存到数据库中并从新初始化数据流。若是网络请求失败,直接发送一个错误。
注:将新的数据保存到磁盘后,要从数据库从新初始化数据流,可是一般不须要这样作,由于数据库将会发送变动。另外一方面,依赖数据库发送变动会有一些很差的反作用,由于在数据没有变化时若是数据库会避免发送更改将会使其中断。咱们也不但愿发送从网络返回的结果,由于这违背的单一数据源原则(即便在数据库中有触发器会改变保存值)。咱们也不但愿在没有新数据的时候发送 SUCCESS,由于这会给客户端发送错误信息。
如下是 NetworkBoundResource 类为其子类提供的公共 API:
// ResultType: Resource 数据的类型
// RequestType: API 响应的类型
public abstract class NetworkBoundResource<ResultType, RequestType> {
// 调用该方法将 API 响应的结果保存到数据库中。
@WorkerThread
protected abstract void saveCallResult(@NonNull RequestType item);
// 调用该方法判断数据库中的数据是否应该从网络获取并更新。
@MainThread
protected abstract boolean shouldFetch(@Nullable ResultType data);
// 调用该方法从数据库中获取缓存数据。
@NonNull @MainThread
protected abstract LiveData<ResultType> loadFromDb();
// 调用该方法建立 API 请求。
@NonNull @MainThread
protected abstract LiveData<ApiResponse<RequestType>> createCall();
// 获取失败时调用。
// 子类可能须要充值组件(如:速率限制器)。
@MainThread
protected void onFetchFailed() {
}
// 返回一个表明 Resource 的 LiveData。
public final LiveData<Resource<ResultType>> getAsLiveData() {
return result;
}
}复制代码
请注意,上述类定义了两个类型参数(ResultType,RequestType),由于从 API 返回的数据类型可能和本地使用的数据类型不一样。
还要注意,上述代码使用 ApiResponse 做为网络请求,ApiResponse 是对于 Retrofit2.Call 类的简单封装,用以将其响应转换为 LiveData。
如下是 NetworkBoundResource 类的其他实现部分。
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();
// 从新附加 dbSource 做为新的来源,
// 它将会迅速发送最新的值。
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) {
// 指定请求一个最新的实时数据。
// 不然,会获得最新的缓存数据,而且可能不会由从网络获取的最新数据更新。
result.addSource(loadFromDb(),
newData -> result.setValue(Resource.success(newData)));
}
}.execute();
}
}复制代码
如今,可使用 NetworkBoundResource 在 Repository 中编写磁盘和网络绑定 User 的实现。
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();
}
}复制代码