google官方地址:https://developer.android.com/jetpack/docs/guide#addendum
本指南适用于过去构建应用程序基础知识的开发人员,现在希望了解构建强大的,生产质量的应用程序的最佳实践和建议的体系结构。
本页假定您熟悉Android Framework。如果您不熟悉应用程序开发,请查看入门培训系列,其中包含本指南的先决条件主题。
与传统的桌面版本不同,在大多数情况下,它们只有一个来自启动器快捷方式的入口点并作为单个整体流程运行,Android应用程序的结构要复杂得多。典型的Android应用程序由多个应用程序组件构成,包括活动,片段,服务,内容提供程序和广播接收器。
大多数这些应用程序组件都在应用程序清单中声明,Android操作系统使用该清单来决定如何将您的应用程序集成到其设备的整体用户体验中。虽然如前所述,桌面应用程序传统上是一个单一的流程,但是正确编写的Android应用程序需要更加灵活,因为用户在设备上编写不同的应用程序,不断切换流程和任务。
例如,考虑当您在自己喜欢的社交网络应用中分享照片时会发生什么。该应用程序触发相机意图,Android操作系统启动相机应用程序来处理请求。此时,用户离开社交网络应用程序,但他们的体验是无缝的。反过来,相机应用程序可能会触发其他意图,例如启动文件选择器,这可能会启动另一个应用程序。最终用户回到社交网络应用程序并共享照片。此外,在此过程中的任何时刻,用户都可能被电话打断,并在完成电话呼叫后回来分享照片。
在Android中,这种跳频行为很常见,因此您的应用必须正确处理这些流量。请记住,移动设备受资源限制,因此在任何时候,操作系统都可能需要杀死某些应用程序以为新设备腾出空间。
所有这一切的重点在于,您的应用程序组件可以单独启动,也可以无序启动,并且可以由用户或系统随时销毁。由于应用程序组件是短暂的,并且它们的生命周期(当它们被创建和销毁时)不在您的控制之下,因此您不应在应用程序组件中存储任何应用程序数据或状态,并且您的应用程序组件不应相互依赖。
如果您无法使用应用程序组件来存储应用程序数据和状态,那么应该如何构建应用程序?
您应该关注的最重要的事情是在您的应用中分离关注点。在a Activity
或a中编写所有代码是一个常见的错误Fragment
。任何不处理UI或操作系统交互的代码都不应该在这些类中。尽可能保持精简可以避免许多与生命周期相关的问题。不要忘了,你不拥有这些类,它们是体现了OS和您的应用程序之间的合同只是胶类。Android操作系统可能会根据用户交互或其他因素(如内存不足)随时销毁它们。最好尽量减少对它们的依赖,以提供可靠的用户体验。
第二个重要原则是您应该从模型中驱动UI,最好是持久模型。持久性是理想的两个原因:如果操作系统破坏您的应用程序以释放资源,您的用户将不会丢失数据,即使网络连接不稳定或未连接,您的应用程序也将继续工作。模型是负责处理应用程序数据的组件。它们独立于应用程序中的视图和应用程序组件,因此它们与这些组件的生命周期问题隔离开来。保持UI代码简单且没有应用程序逻辑,使其更易于管理。将您的应用程序基于具有明确定义的数据管理职责的模型类,将使其可测试且您的应用程序保持一致。
在本节中,我们将演示如何使用Architecture Components通过用例来构建应用程序。
注意:没有一种方法可以编写最适合每种情况的应用程序。话虽这么说,这个推荐的架构应该是大多数用例的一个很好的起点。如果您已经有了编写Android应用程序的好方法,则无需更改。
想象一下,我们正在构建一个显示用户配置文件的UI。此用户配置文件将使用REST API从我们自己的私有后端获取。
UI将包含一个片段UserProfileFragment.java
及其相应的布局文件user_profile_layout.xml
。
为了驱动UI,我们的数据模型需要包含两个数据元素。
我们将创建一个UserProfileViewModel
基于ViewModel类来保存此信息。
甲视图模型提供了一个特定的UI组件中的数据,如一个片段或活性,和处理与数据处理的部分业务,如主叫其他组件加载数据或转发的用户修改的通信。ViewModel不了解View,也不受配置更改的影响,例如由于轮换而重新创建活动。
现在我们有3个文件。
user_profile.xml
:屏幕的UI定义。UserProfileViewModel.java
:为UI准备数据的类。UserProfileFragment.java
:UI控制器,它在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 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的用户字段时,我们需要一种方法来通知UI。这是LiveData类的用武之地。
LiveData是一个可观察的数据持有者。它允许应用程序中的组件观察
LiveData
对象以进行更改,而无需在它们之间创建明确且严格的依赖路径。LiveData还尊重应用程序组件(活动,片段,服务)的生命周期状态,并做正确的事情来防止对象泄漏,以便您的应用程序不会消耗更多内存。
注意:如果您已经在使用像RxJava或 Agera这样的库 ,则可以继续使用它们而不是LiveData。但是当您使用它们或其他方法时,请确保正确处理生命周期,以便在相关LifecycleOwner停止时数据流暂停,并在销毁LifecycleOwner时销毁流。您还可以添加 android.arch.lifecycle:reactivestreams
工件以将LiveData与另一个反应流库(例如,RxJava2)一起使用。
现在我们将a中的User
字段替换为UserProfileViewModel
a, LiveData
以便在更新数据时通知片段。最棒的LiveData
是它可以识别生命周期,并且在不再需要时会自动清理引用。
public class UserProfileViewModel extends ViewModel {
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 -> {
// update UI
});
}
每次更新用户数据时, 都会调用onChanged回调并刷新UI。
如果您熟悉使用可观察回调的其他库,您可能已经意识到我们不必覆盖片段的onStop()
方法来停止观察数据。LiveData不需要这样做,因为它可以识别生命周期,这意味着除非片段处于活动状态(已接收onStart()
但未接收onStop()
),否则它不会调用回调。片段收到时,LiveData也会自动删除观察者onDestroy()
。
我们也没有做任何特殊处理配置更改(例如,用户旋转屏幕)。ViewModel会在配置更改时自动恢复,因此只要新片段生效,它就会收到相同的ViewModel实例,并且会立即使用当前数据调用回调。这就是ViewModels不应该直接引用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类负有太多责任,这违背了我们之前提到的关注点分离原则。另外,视图模型的范围是依赖于一个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
可能不是唯一需要的课程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;
}
}
上面的存储库实现适用于抽象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操作系统杀死进程后数小时后回来会发生什么?
通过当前的实现,我们需要从网络中再次获取数据。这不仅是一种糟糕的用户体验,而且还浪费,因为它将使用移动数据重新获取相同的数据。您可以通过缓存Web请求来解决此问题,但这会产生新问题。如果相同的用户数据显示来自另一种类型的请求(例如,获取朋友列表)会发生什么?然后,您的应用可能会显示不一致的数据,这充其量只是令人困惑的用户体验。例如,相同用户的数据可能不同地显示,因为可以在不同时间执行朋友列表请求和用户请求。您的应用需要合并它们以避免显示不一致的数据。
处理此问题的正确方法是使用持久模型。这是 Room persistence library来救援的地方。
Room是一个对象映射库,它提供本地数据持久性和最少的样板代码。在编译时,它根据模式验证每个查询,以便损坏的SQL查询导致编译时错误而不是运行时失败。Room抽象了使用原始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
是抽象的。Room自动提供它的实现。有关详细信息,请参阅房间文档
现在我们需要一种将用户数据插入数据库的方法。为此,我们将创建一个数据访问对象(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
。
现在我们的代码已经完成。如果用户几天后回到相同的用户界面,他们会立即看到用户信息,因为我们已经保留了用户信息。同时,如果数据陈旧,我们的存储库将在后台更新数据。当然,根据您的使用情况,如果持久数据太旧,您可能不希望显示这些数据。
在某些用例中,例如pull-to-refresh,如果当前正在进行网络操作,则UI必须向用户显示。将UI操作与实际数据分开是一种很好的做法,因为它可能由于各种原因而更新(例如,如果我们获取朋友列表,则可能再次获取相同的用户来触发LiveData
更新)。从UI的角度来看,飞行中有请求的事实只是另一个数据点,类似于任何其他数据(如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版本不同。
Web服务:让测试独立于外部世界非常重要,因此即使您的Webservice
测试也应避免对后端进行网络调用。有很多库可以帮助解决这个问题。例如, MockWebServer 是一个很棒的库,可以帮助您为测试创建虚假的本地服务器。
测试工件架构组件提供了一个maven工件来控制其后台线程。在android.arch.core:core-testing
工件内部 ,有2个JUnit规则:
InstantTaskExecutorRule
:此规则可用于强制体系结构组件立即在调用线程上执行任何后台操作。CountingTaskExecutorRule
:此规则可用于检测测试,以等待架构组件的后台操作或将其作为空闲资源连接到Espresso。下图显示了我们推荐的体系结构中的所有模块以及它们如何相互交互:
编程是一个创造性的领域,构建Android应用程序也不例外。有许多方法可以解决问题,无论是在多个活动或片段之间传递数据,检索远程数据并在本地持久保存以用于脱机模式,还是任何其他非常重要的应用程序遇到的常见场景。
虽然以下建议不是强制性的,但我们的经验是,遵循它们将使您的代码库从长远来看更加强大,可测试和可维护。
在上面推荐的应用程序架构部分中,我们故意省略网络错误和加载状态以保持样本简单。在本节中,我们演示了一种使用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;
}
}
现在,我们可以使用use 在存储库中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();
}
}