本指南适用于过去构建应用程序的基础知识的开发人员,现在想知道构建强大的生产质量应用程序的最佳实践和建议的体系结构。
注意:本指南假定读者熟悉Android框架。如果您不熟悉应用程序开发,请查看入门培训系列,其中包含本指南的必备主题。
应用开发者面临的常见问题
与传统的桌面应用程序不同,在大多数情况下,这些应用程序从启动程序的快捷方式中有一个入口点,并且作为单个单一进程运行,Android应用程序的结构更为复杂。一个典型的Android应用程序由多个应用程序组件构成,包括活动,片段,服务,内容提供商和广播接收器。
大多数这些应用程序组件都是在Android操作系统使用的应用程序清单中声明的,以决定如何将您的应用程序与其设备的整体用户体验相集成。虽然如前所述,桌面应用程序传统上是作为一个整体流程运行的,但正确编写的Android应用程序需要更灵活,因为用户可以通过设备上的不同应用程序进行编程,不断切换流程和任务。
例如,请考虑在您最喜爱的社交网络应用中分享照片时会发生什么。该应用程序会触发相机意图,Android操作系统将启动相机应用程序来处理请求。此时,用户离开了社交网络应用程序,但他们的体验是无缝的。反过来,相机应用程序可能会触发其他意图,例如启动文件选择器,该文件选择器可能会启动另一个应用程序。最终用户回到社交网络应用并分享照片。此外,用户可能会在此过程的任何时候通过电话打断电话,并在完成电话后返回分享照片。
在Android中,这种应用程序跳转行为很常见,所以您的应用程序必须正确处理这些流程。请记住,移动设备是资源受限的,所以在任何时候,操作系统可能需要杀死一些应用程序,为新的应用程序腾出空间。
所有这一切都意味着您的应用程序组件可以单独和无序地启动,并且可以在任何时候由用户或系统销毁。因为应用程序组件是短暂的,并且它们的生命周期(创建和销毁时)不受您的控制,所以您不应该在应用程序组件中存储任何应用程序数据或状态,并且应用程序组件不应相互依赖。
共同的建筑原则
如果您无法使用应用程序组件来存储应用程序数据和状态,应该如何构建应用程序?
你应该关注的最重要的事情是在应用程序中分离关注点。将所有代码写入一个Activity
或一个常见错误是一个常见的错误Fragment
。任何不处理UI或操作系统交互的代码都不应该在这些类中。尽可能保持精简可以避免许多生命周期相关的问题。不要忘记,您不拥有这些类,它们只是体现操作系统和您的应用程序之间合同的胶合类。基于用户交互或其他因素(如低内存),Android操作系统可能随时销毁它们。最好尽量减少对它们的依赖,以提供可靠的用户体验。
第二个重要原则是你应该从一个模型驱动你的UI,最好是一个持久模型。持久性非常理想,原因有两个:如果操作系统销毁您的应用程序以释放资源,并且您的应用程序即使在网络连接状况不佳或未连接时,您的应用程序仍将继续工作,则用户不会丢失数据。模型是负责处理应用程序数据的组件。它们独立于应用程序中的视图和应用程序组件,因此它们与这些组件的生命周期问题相隔离。保持UI代码简单并且没有应用程序逻辑,这使得它更易于管理。将您的应用程序放在模型类上,具有明确的数据管理责任将使它们可测试并且您的应用程序保持一致。
推荐的应用架构
在本节中,我们将演示如何使用体系结构组件构建应用程序,方法是处理用例。
注意:不可能有一种编写适用于所有场景的最佳应用程序。这就是说,这个推荐的架构应该是大多数用例的一个很好的起点。如果您已经有了编写Android应用程序的好方法,则无需更改。
想象一下,我们正在构建一个显示用户配置文件的用户界面。该用户配置文件将使用REST API从我们自己的私人后端获取。
构建用户界面
UI将由一个片段UserProfileFragment.java
及其相应的布局文件组成user_profile_layout.xml
。
为了驱动用户界面,我们的数据模型需要保存两个数据元素。
- 用户ID:用户的标识符。最好使用片段参数将此信息传递到片段中。如果Android操作系统破坏你的程序,这些信息将被保留下来,以便在你的应用程序下次重新启动时该ID可用。
- 用户对象:保存用户数据的POJO。
我们将创建一个UserProfileViewModel
基于ViewModel类来保存这些信息。
甲ViewModel提供了一个特定的UI组件中的数据,如一个片段或活性,和处理与数据处理的部分业务,如主叫其他组件加载数据或转发的用户修改的通信。ViewModel不知道视图,并且不受配置更改的影响,例如由于旋转而重新创建活动。
现在我们有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 Fragment {
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);
}
}
现在,我们有这三个代码模块,我们如何连接它们?毕竟,当设置ViewModel的用户字段时,我们需要一种方法来通知用户界面。这是LiveData类的用途。
LiveData是一个可观察的数据持有者。它允许应用程序中的组件观察
LiveData
对象以进行更改,而不会在它们之间创建明确和严格的依赖关系路径。LiveData还尊重应用程序组件(活动,片段和服务)的生命周期状态,并做正确的事情来防止对象泄漏,以便您的应用程序不会消耗更多内存。
注意:如果您已经在使用类似RxJava或 Agera的库 ,则可以继续使用它们而不是LiveData。但是,当您使用它们或其他方法时,请确保正确处理生命周期,以便在相关的LifecycleOwner停止时数据流暂停,并且在LifecycleOwner销毁时数据流被销毁。您还可以添加android.arch.lifecycle:reactivestreams
工件以将LiveData与另一个反应流库(例如RxJava2)一起使用。
现在我们用a 替换该User
字段,以便在数据更新时通知片段。最重要的 是它能够感知生命周期,并在不再需要时自动清除引用。UserProfileViewModel
LiveData
LiveData
public class UserProfileViewModel extends ViewModel {
...
private User user;
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。
如果您熟悉使用可观察回调的其他库,您可能已经意识到我们不必重写片段的onStop()
方法来停止观察数据。对于LiveData来说这不是必需的,因为它是生命周期感知的,这意味着它不会调用回调,除非片段处于活动状态(已接收onStart()
但未接收onStop()
)。当数据片段收到时,LiveData也会自动移除观察者onDestroy()
。
我们也没有做任何特殊的事情来处理配置变化(例如,用户旋转屏幕)。当配置发生变化时,ViewModel会自动恢复,因此一旦新的片段生效,它将接收到同一个ViewModel实例,并且将立即使用当前数据调用该回调。这就是为什么ViewModel不应直接引用Views的原因; 他们可以超越View的生命周期。请参阅 ViewModel的生命周期。
获取数据
现在我们已将ViewModel连接到片段,但ViewModel如何获取用户数据?在这个例子中,我们假设我们的后端提供了一个REST API。我们将使用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 getUser(@Path("user") String userId);
}
一个天真的实现ViewModel
可以直接调用Webservice
来获取数据并将其分配给用户对象。即使它可行,您的应用程序在增长时也很难保持。它给ViewModel类提供了太多的责任,这违背了前面提到的关注点分离原则 。此外,ViewModel的范围与一个Activity
或一个Fragment
生命周期相关联,因此在生命周期结束时丢失所有数据是一种糟糕的用户体验。相反,我们的ViewModel会将这项工作委托给一个新的Repository模块。
存储库模块负责处理数据操作。他们为应用程序的其他部分提供了一个干净的API。他们知道从何处获取数据以及在更新数据时调用哪些API。您可以将它们视为不同数据源(持久模型,Web服务,缓存等)之间的中介。
UserRepository
下面的类使用WebService
获取用户数据项。
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;
}
}
尽管存储库模块看起来没有必要,但它有一个重要的目的; 它会从应用程序的其余部分中提取数据源。现在我们的ViewModel并不知道数据是由该数据获取的Webservice
,这意味着我们可以根据需要将其交换为其他实现。
注意:为简单起见,我们忽略了网络错误情况。有关公开错误和加载状态的替代实现,请参阅 附录:公开网络状态。
管理组件之间的依赖关系:
UserRepository
上面的类需要一个实例Webservice
来完成它的工作。它可以简单地创建它,但要做到这一点,它也需要知道Webservice
类的依赖关系来构造它。这会使代码复杂化并重复(例如,每个需要Webservice
实例的类 都需要知道如何用它的依赖关系来构造它)。另外,UserRepository
可能不是唯一需要a的类Webservice
。如果每个班级创建一个新的WebService
,这将是非常重要的资源。
有两种模式可以用来解决这个问题:
- 依赖注入:依赖注入允许类在不构建它们的情况下定义它们的依赖关系。在运行时,另一个类负责提供这些依赖关系。我们推荐Google的Dagger 2库在Android应用中实现依赖注入。Dagger 2通过遍历依赖关系树自动构造对象,并为依赖关系提供编译时保证。
- 服务定位器:服务定位器提供了一个注册表,类可以获得它们的依赖关系而不是构建它们。它比依赖注入(DI)实现起来相对容易,所以如果您不熟悉DI,请改用Service Locator。
这些模式允许您扩展代码,因为它们提供了用于管理依赖关系的清晰模式,无需复制代码或增加复杂性。它们都允许交换实现进行测试; 这是使用它们的主要好处之一。
在这个例子中,我们将使用Dagger 2来管理依赖关系。
连接ViewModel和存储库
现在我们修改我们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;
}
}
缓存数据
上面的存储库实现对抽象调用Web服务很好,但因为它只依赖一个数据源,所以它不是非常实用。
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将立即可见,因为存储库将从内存中缓存中检索数据。但是,如果用户离开应用程序并在Android OS终止该进程后数小时后回来,会发生什么情况?
在目前的实施中,我们需要再次从网络中获取数据。这不仅是糟糕的用户体验,而且还会浪费,因为它将使用移动数据重新获取相同的数据。你可以简单地通过缓存Web请求来解决这个问题,但它会产生新的问题。如果相同的用户数据显示来自另一种类型的请求(例如,获取朋友列表),会发生什么情况?那么你的应用程序可能会显示不一致的数据,这充其量是令人困惑的用户体验。例如,同一用户的数据可能会以不同的方式显示,因为朋友列表请求和用户请求可能会在不同的时间执行。您的应用需要合并它们以避免显示不一致的数据。
处理这个问题的正确方法是使用持久模型。这是 Room持久性库来拯救的地方。
Room是一个对象映射库,它使用最少的样板代码提供本地数据持久性。在编译时,它会针对模式验证每个查询,以便断开的SQL查询导致编译时错误而不是运行时失败。会议室抽象出一些使用原始SQL表和查询的底层实现细节。它还允许观察对数据库数据(包括集合和连接查询)的更改,通过LiveData对象公开这些更改 。另外,它明确定义了解决常见问题的线程约束,例如访问主线程上的存储。
注意:如果您的应用程序已经使用另一个持久性解决方案(如SQLite对象关系映射(ORM)),则不需要使用Room替换现有的解决方案。但是,如果您正在编写新应用程序或重构现有应用程序,我们建议使用Room来保存应用程序的数据。这样,您可以利用库的抽象和查询验证功能。
要使用Room,我们需要定义我们的本地模式。首先,注释User
该类以@Entity
将其标记为数据库中的表。
@Entity
class User {
@PrimaryKey
private int id;
private String name;
private String lastName;
// getters and setters for fields
}
然后,通过扩展RoomDatabase
您的应用程序来创建数据库类 :
@Database(entities = {User.class}, version = 1)
public abstract class MyDatabase extends RoomDatabase {
}
注意这MyDatabase
是抽象的。房间自动提供它的实施。详情请见房间文件。
现在我们需要一种将用户数据插入数据库的方法。为此,我们将创建一个数据访问对象(DAO)。
@Dao
public interface UserDao {
@Insert(onConflict = REPLACE)
void save(User user);
@Query("SELECT * FROM user WHERE id = :userId")
LiveData load(String userId);
}
然后,从我们的数据库类中引用DAO。
@Database(entities = {User.class}, version = 1)
public abstract class MyDatabase extends RoomDatabase {
public abstract UserDao userDao();
}
注意该load
方法返回一个LiveData
。房间知道数据库何时被修改,当数据改变时它会自动通知所有活动的观察者。因为它使用的是LiveData,所以这将很有效,因为它只会在至少有一个活动观察者的情况下更新数据。
注意:房间会根据表格修改检查失效,这意味着它可能会发送误报通知。
现在我们可以修改我们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 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
。
现在我们的代码已完成。如果用户以后再回到相同的用户界面,他们会立即看到用户信息,因为我们坚持了它。同时,如果数据陈旧,我们的存储库将在后台更新数据。当然,根据您的使用情况,如果数据太旧,您可能不希望显示持久数据。
在某些使用情况下,如拉到刷新,UI显示用户是否正在进行网络操作非常重要。将UI操作与实际数据分开是一种很好的做法,因为它可能因各种原因而更新(例如,如果我们获取朋友列表,可能会再次触发同一用户触发LiveData
更新)。从用户界面的角度来看,有一个请求在飞行中的事实只是另一个数据点,类似于任何其他数据(如User
对象)。
这个用例有两种常见的解决方案:
- 更改
getUser
为返回包含网络操作状态的LiveData。附录中提供了一个示例实现:显示网络状态部分。 - 在存储库类中提供另一个可以返回用户刷新状态的公共函数。如果只想响应显式用户操作(如拉到刷新)来显示网络状态,则此选项更好。
单一的事实来源
不同的REST API端点通常会返回相同的数据。例如,如果我们的后端拥有另一个返回朋友列表的终端,则同一用户对象可能来自两个不同的API终端,可能具有不同的粒度。如果要按原样UserRepository
返回Webservice
请求的响应,那么我们的UI可能会显示不一致的数据,因为这些请求之间的数据可能在服务器端发生更改。这就是为什么在UserRepository
实现中,Web服务回调只是将数据保存到数据库中。然后,对数据库的更改将触发活动LiveData对象上的回调。
在这个模型中,数据库充当真相的单一来源,应用程序的其他部分通过存储库访问它。无论您是否使用磁盘缓存,我们都建议您的存储库将数据源指定为应用程序其余部分的单一来源。
测试
我们已经提到分离的好处之一就是可测试性。让我们看看我们如何测试每个代码模块。
用户界面和交互:这将是您唯一需要 Android UI Instrumentation测试的时间。测试UI代码的最好方法是创建一个 Espresso测试。您可以创建片段并为其提供模拟ViewModel。由于该片段只与ViewModel交谈,因此嘲笑它将足以完全测试此UI。
ViewModel:可以使用JUnit测试来测试ViewModel 。你只需要嘲笑
UserRepository
测试它。UserRepository:您也可以
UserRepository
使用JUnit测试来测试。你需要模拟Webservice
和DAO。您可以测试它是否进行了正确的Web服务调用,将结果保存到数据库中,并且如果数据已缓存且最新,则不会发出任何不必要的请求。由于这两个Webservice
和UserDao
的界面,你可以嘲笑他们或创建更复杂的测试案例假冒实现..-
UserDao:测试DAO类的推荐方法是使用仪器测试。由于这些仪器测试不需要任何UI,它们仍然可以运行得很快。对于每个测试,您都可以创建一个内存数据库,以确保测试没有任何副作用(如更改磁盘上的数据库文件)。
Room还允许指定数据库实现,以便通过为其提供JUnit实现来测试它
SupportSQLiteOpenHelper
。通常不推荐使用此方法,因为设备上运行的SQLite版本可能与主机上的SQLite版本不同。 Webservice:使测试独立于外部环境非常重要,所以即使您的
Webservice
测试也应该避免对后端进行网络调用。有很多图书馆可以帮助解决这个问题。例如, MockWebServer 是一个伟大的库,可以帮助您为测试创建一个假本地服务器。-
测试工件体系结构组件提供了一个Maven工件来控制其后台线程。在
android.arch.core:core-testing
神器内部 ,有2个JUnit规则:-
InstantTaskExecutorRule
:此规则可用于强制架构组件立即在调用线程上执行任何后台操作。 -
CountingTaskExecutorRule
:此规则可用于检测测试,以等待体系结构组件的后台操作或将其作为闲置资源连接到Espresso。
-
最终的体系结构
下图显示了我们推荐的体系结构中的所有模块以及它们如何相互交互:
指导原则
编程是一个创造性领域,构建Android应用程序并不例外。有很多方法可以解决问题,无论是在多个活动或片段之间传递数据,检索远程数据并将其保存在本地以用于脱机模式,还是其他许多其他常见应用程序遇到的情况。
虽然以下建议不是强制性的,但我们的经验是,遵循这些建议将使您的代码基础更加健壮,可测试并可长期维护。
- 您在清单中定义的入口点 - 活动,服务,广播接收器等 - 不是数据的来源。相反,他们只应该协调与该入口点相关的数据子集。由于每个应用程序组件的寿命相当短,这取决于用户与设备的交互以及运行时的整体当前运行状况,因此您不希望任何这些入口点成为数据源。
- 无情地在应用程序的各个模块之间创建明确界定的责任。例如,不要将代码从网络中加载到代码库中的多个类或包中。同样,不要将不相关的职责 - 例如数据缓存和数据绑定 - 放到同一个类中。
- 尽可能少地暴露每个模块。不要试图创建“只有那一个”快捷方式,从一个模块公开内部实现细节。您可能在短期内获得一些时间,但随着代码库的发展,您将多次支付技术债务。
- 在定义模块之间的交互时,请考虑如何让每个模块单独进行测试。例如,有一个定义良好的API从网络中获取数据将使测试将数据保存在本地数据库中的模块变得更加容易。相反,如果将来自这两个模块的逻辑混合在一起,或者在整个代码库中使用网络代码,那么测试起来会更加困难 - 即使不是不可能 - 也是如此。
- 你的应用程序的核心是什么让它从其他中脱颖而出。不要花太多时间重复发明轮子或一次又一次地写出相同的样板代码。相反,将精力集中在让您的应用独特的东西上,让Android Architecture组件和其他推荐的库处理重复的样板。
- 尽可能多地保留相关和新鲜的数据,以便在设备处于离线模式时可以使用您的应用程序。虽然您可以享受持续高速的连接,但您的用户可能不会。
- 您的存储库应该指定一个数据源作为单一的事实来源。只要你的应用程序需要访问这部分数据,它应该始终来自单一的事实来源。有关更多信息,请参阅单一真相源。
附录:揭示网络状态
在上面推荐的应用程序体系结构部分中,我们故意省略了网络错误和加载状态,以便简化示例。在本节中,我们将演示一种使用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);
}
}
因为从网络中加载数据并从磁盘中加载数据是一种常见的用例,我们将创建一个NetworkBoundResource
可以在多个地方重复使用的帮助类。以下是决策树 NetworkBoundResource
:
它通过观察资源的数据库开始。当条目从数据库第一次加载时,NetworkBoundResource
检查结果是否足够好以便分派和/或应该从网络中获取。请注意,这两种情况可能同时发生,因为您可能希望在从网络更新缓存数据时显示缓存数据。
如果网络呼叫成功完成,它会将响应保存到数据库中并重新初始化流。如果网络请求失败,我们直接发送失败。
注意:在将新数据保存到磁盘后,我们会重新初始化数据库中的数据流,但通常我们不需要这样做,因为数据库将分发更改。另一方面,依靠数据库来发送更改将依赖于不好的副作用,因为如果数据没有改变,数据库可以避免调度更改,则副作用可能会中断。我们也不想派遣从网络到达的结果,因为这将违背单一的事实来源(也许在数据库中有触发器会改变保存的值)。我们也不希望在SUCCESS
没有新数据的情况下发送消息,因为它会向客户端发送错误信息。
以下是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> 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, implemented
// in the base class.
public final LiveData> getAsLiveData();
}
请注意,上面的类定义了两个类型参数(ResultType
, RequestType
),因为从API返回的数据类型可能与本地使用的数据类型不匹配。
另请注意,上面的代码ApiResponse
用于网络请求。 ApiResponse
是一个简单的Retrofit2.Call
类包装来将其响应转换为LiveData。
以下是该NetworkBoundResource
课程的其余部分:
public abstract class NetworkBoundResource {
private final MediatorLiveData> result = new MediatorLiveData<>();
@MainThread
NetworkBoundResource() {
result.setValue(Resource.loading(null));
LiveData 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 dbSource) {
LiveData> 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 response) {
new AsyncTask() {
@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();
}
public final LiveData> getAsLiveData() {
return result;
}
}
现在,我们可以使用用于在存储库中NetworkBoundResource
编写我们的磁盘和网络绑定 User
实现。
class UserRepository {
Webservice webservice;
UserDao userDao;
public LiveData> loadUser(final String userId) {
return new NetworkBoundResource() {
@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 loadFromDb() {
return userDao.load(userId);
}
@NonNull @Override
protected LiveData> createCall() {
return webservice.getUser(userId);
}
}.getAsLiveData();
}
}