Android应用架构简介java
对于通过过构建app的Android开发人员来讲, 如今是时候了解一下构建鲁棒, 质量高的应用的最佳实践和推荐架构了.react
这篇文章假设读者对Android framework比较熟悉.android
OK, let's begin!web
App开发人员面临的常见问题数据库
传统的桌面开发, 在大多数状况下, 拥有一个来自Launcher快捷键的单独入口点, 并在独立的总体进程中运行. 而Android应用则拥有更多复杂的结构. 典型的Android应用由多个应用构件组成, 包括Activities, Fragments, Services, ContentProviders和BroadcastReceivers.编程
大多数这些应用构件声明在AndroidManifest文件中, 该文件被Android系统使用以将应用整合进全面的用户体验中. 尽管, 如先前所言, 传统的桌面应用做为一个完整的进程运行, 而正确书写的Android应用须要更多的灵活性, 由于用户经过频繁地切换流和任务, 在设备上不一样的应用间编写不一样的路径.api
好比, 当你在最喜欢的社交网络应用上分享照片时, 想一想会发生什么吧. 应用触发了一个相机Intent, Android系统经过这个Intent打开相机应用来处理这个请求. 此时, 用户离开了社交网络应用, 可是体验是无缝联接的. 以后, 相机应用可能触发其它的Intent, 好比打开文件选择器, 而文件选择器可能打开了其它应用. 最后用户回到了社交网络应用并分享照片. 并且, 在这些进程, 用户可能随时被电话打断, 以后在打完电话后返回分享照片.缓存
在Android中, 应用跳跃行为非常广泛, 因此你的应用必须正确地处理这些流程. 谨记: 移动设备是资源限制的, 由此在任意时刻, 操做系统须要杀掉一些应用, 为新的应用腾出空间.服务器
关键点是: 应用构件可以被独立且无序地打开, 并且在任意时刻都可以被用户或者系统销毁. 由于应用构件是瞬息的, 它们的生命周期(控件被建立和销毁的时刻)并不受你控制, 由此, 在应用构件中不该该存储任何应用数据和状态, 并且应用构件之间不该该依赖于彼此.网络
通用架构规则
若是你不能使用应用构件保存应用数据和状态, 应用应该怎么组织?
你应该关注的最重要的事件是:关注分离. 常见的一个错误时: 在单个Activity/Fragment中写所有的代码. 任何不处理UI和操做系统交互的代码不该该写在这些类里面. 尽量保持简洁会容许你避免生命周期相关的问题. 不要忘记: 你不拥有这些类, 它们仅仅是胶水类, 象征了操做系统和应用之间的协议. Android操做系统可能随时基于用户交互或者其它诸如内存不足等因素销毁它们. 要提供稳定的用户体验, 最好最小化对它们的依赖.
第二个重要的规则是: 使用模型, 尤为是持久化模型驱动UI. 持久化因两个缘由而完美: 若是操做系统销毁了应用以释放资源, 用户不会丢失数据; 在网络链接弱或者未链接时, 应用可以持续工做. 而模型就是为用户处理数据的构件. 它们独立于应用中的视图和应用构建, 由此它们绝缘于这些构件的生命周期问题. 保持UI代码简单且独立于应用逻辑使得管理更加简便. 经过定义良好的数据管理职责, 将应用基于模型类, 将使得模型类更加容易测试, 使得应用更具一致性.
推荐的应用架构
下面将经过用例使用架构组件来组织应用.
备注: 不可能有一种写应用的方式对全部场景都是最好的. 也就是说, 这里推荐的架构对于大多数用例应该是好的起点. 若是你已经有了好找写Android应用的方式, 你无需改变.
想像一下, 构建一个展现用户概况的UI. 用户概况将会使用REST API从自有私有后台拉取.
构建user interface
UI由UserProfileFragment.java和对应的user_profile_layout.xml组成.
要驱动UI, 咱们的数据模型须要持有两个数据元素.
咱们会基于ViewModel类建立UserProfileViewModel来保持信息.
ViewModel为特定的UI构件, 诸如Activity/Fragment, 提供数据, 并处理与数据处理的业务部分的通信, 诸如调用其它构件加载数据或者提交用户修改. ViewModel并不知晓视图, 且并不受诸如因为屏幕旋转致使的Activity重建等配置改变的影响.
如今咱们有三个文件:
下面是起始实现(为简单起见, layout文件没有提供):
1 public class UserProfileViewModel extends ViewModel { 2 private String userId; 3 private User user; 4 5 public void init(String userId) { 6 this.userId = userId; 7 } 8 public User getUser() { 9 return user; 10 } 11 }
1 public class UserProfileFragment extends Fragment { 2 private static final String UID_KEY = "uid"; 3 private UserProfileViewModel viewModel; 4 5 @Override 6 public void onActivityCreated(@Nullable Bundle savedInstanceState) { 7 super.onActivityCreated(savedInstanceState); 8 String userId = getArguments().getString(UID_KEY); 9 viewModel = ViewModelProviders.of(this).get(UserProfileViewModel.class); 10 viewModel.init(userId); 11 } 12 13 @Override 14 public View onCreateView(LayoutInflater inflater, 15 @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { 16 return inflater.inflate(R.layout.user_profile, container, false); 17 } 18 }
如今, 咱们有三个代码模块, 咱们该如何链接? 毕竟, ViewModel的user域设置的时候, 咱们须要一种方式通知UI. 这就是LiveData出现的地方:
LiveData是可观测的数据持有者. 它让应用组件观测LiveData对象的改变, 却没有建立显著且严格的依赖路径. LiveData也尊重应用组件的生命周期状态, 作正确的事防止对象泄露, 保证应用并没消费更多的内存.
备注: 若是你在使用诸如RxJava/Agera库, 你能够继续使用它们而没必要使用LiveData. 可是当你使用它们和其它的途径的时候, 确保你在正确地处理生命周期, 确保在相关的LifecycleOwner中止的时候, 数据流暂停; 在LifecycleOwner销毁的时候, 数据流被销毁. 你也能够添加 android.arch.lifecycle:reactivestreams 和其它的reactive库(好比, RxJava2)一块儿使用LiveData.
如今咱们使用LiveData<User>取代UserProfileViewModel里的User域, 确保在数据更新的时候, fragment可以被通知到. LiveData的一个好处是: 它是可感知生命周期的, 在引用再也不须要的时候, LiveData会自动地清除它们.
1 public class UserProfileViewModel extends ViewModel { 2 ... 3 private User user; 4 private LiveData<User> user; 5 public LiveData<User> getUser() { 6 return user; 7 } 8 }
如今修改UserProfileFragment, 来观测数据并更新UI.
1 @Override 2 public void onActivityCreated(@Nullable Bundle savedInstanceState) { 3 super.onActivityCreated(savedInstanceState); 4 viewModel.getUser().observe(this, user -> { 5 // update UI 6 }); 7 }
每一次用户数据更新, onChanged()回调都会被触发, UI会被刷新.
若是你熟悉于其它的使用可观测回调的库, 你也许已经发觉咱们没必要覆盖fragment的onStop()方式来中止观测数据. LiveData也是没必要要的, 由于它是可以感知生命周期的, 这意味着回调不会被调用, 除非fragment处于活跃状态(接收onStart()方法却没有接收到onStop()方法). 在fragment接收到onDestroy()时, LiveData也会自动地删除观测者.
咱们没必要作任何特殊的事情来处理配置改变(好比, 使用旋转屏). 在配置改变或者fragment恢复生机的时候, ViewModel会自动地保存. 它会接收到相同的ViewModel实例, 回调会自动地使用当前数据回调. 这就是为何不该该直接引用视图; 它们比视图的生命周期活得更久.
拉取数据
如今已经将ViewModel链接到Fragment, 可是ViewModel若是拉取用户数据呢? 在这个例子中, 假设后台提供的是REST API. 咱们会使用Retrofit库来访问后台, 尽管你可使用不一样的库来实现相同的目标.
这是咱们的Retrofit Webservice, 来和后台通讯:
1 public interface Webservice { 2 /** 3 * @GET declares an HTTP GET request 4 * @Path("user") annotation on the userId parameter marks it as a 5 * replacement for the {user} placeholder in the @GET path 6 */ 7 @GET("/users/{user}") 8 Call<User> getUser(@Path("user") String userId); 9 }
实现ViewModel的每个想法也是是直接调用Webservice, 来拉取数据, 并把它赋值给用户对象. 尽管这样能够工做, 可是当随着应用不断增加, 维护将变得很困难. 这样会给予ViewModel太多的责任, 而ViewModel是违反先前提到的"关注分离"原则. 除此以外, ViewModel的范围绑定到了Activity/Fragment, 因此在生命周期结束的时候失去数据是个不好的用户体验. 相反, ViewModel会代码这个工做到一个新Repository模块.
Repository模型负责处理数据操做. 它们给应用提供了干净的API. 它们知道从哪获取数据, 知道在数据更新时, 调用什么API. 你能够把它们看成不一样数据源(持久化模型, 网页服务, 缓存等)之间的中介.
以下UserRepository类使用了Webservice拉取用户数据项.
1 public class UserRepository { 2 private Webservice webservice; 3 // ... 4 public LiveData<User> getUser(int userId) { 5 // This is not an optimal implementation, we'll fix it below 6 final MutableLiveData<User> data = new MutableLiveData<>(); 7 webservice.getUser(userId).enqueue(new Callback<User>() { 8 @Override 9 public void onResponse(Call<User> call, Response<User> response) { 10 // error case is left out for brevity 11 data.setValue(response.body()); 12 } 13 }); 14 return data; 15 } 16 }
尽管Repository模块看起来很没必要要, 但它服务于一个重要目的; 它抽象了数据源. 如今, 咱们的ViewModel并不知道被Webservice拉取的数据, 这意味着咱们有必要将它转化成别的实现.
管理组件之间的依赖
以上的UserRepository类须要Webservice实例来作它的工做. 能够简单地建立它, 但若是要这么作的话, 须要知道Webservice类以构建该类. 这样作会显著地使代码更加复杂和重复(每个须要Webservice实例的类须要知道如何使用它的依赖来构建类). 除此以外, UserRepository极可能不是惟一须要Webservice的类. 若是每个类都建立了新的Webservice, 这将很是消耗资源.
你可使用两种方式解决这个问题:
这些模式容许你衡量本身的代码, 由于他们为在没有重复代码和添加复杂性的的前提下管理依赖提供了清晰的模式. 二者都容许将实现转换成测试; 这是使用二者的主要好处.
链接ViewModel和Repository
下面代码展现了如何修改UserProfileViewModel以使用repository.
1 public class UserProfileViewModel extends ViewModel { 2 private LiveData<User> user; 3 private UserRepository userRepo; 4 5 @Inject // UserRepository parameter is provided by Dagger 2 6 public UserProfileViewModel(UserRepository userRepo) { 7 this.userRepo = userRepo; 8 } 9 10 public void init(String userId) { 11 if (this.user != null) { 12 // ViewModel is created per Fragment so 13 // we know the userId won't change 14 return; 15 } 16 user = userRepo.getUser(userId); 17 } 18 19 public LiveData<User> getUser() { 20 return this.user; 21 } 22 }
缓存数据
以上的repository实现对于抽象对于网络服务的调用是有好处的, 由于它只依赖了惟一一个数据源, 但这并不十分有不少用处.
使用以上UserRepository实现的问题是, 在拉取了数据以后, 并不保存数据. 若是用户离开了UserProfileFragment而后再回来, 应用不会从新拉取数据. 这是很差的, 由于: 它既浪费了宝贵的网络带宽, 又强制用户等待查询的完成. 要解决这个问题, 咱们将向UserRepository添加新的数据源, 而新数据源将会在内存中缓存User对象.
1 @Singleton // informs Dagger that this class should be constructed once 2 public class UserRepository { 3 private Webservice webservice; 4 // simple in memory cache, details omitted for brevity 5 private UserCache userCache; 6 public LiveData<User> getUser(String userId) { 7 LiveData<User> cached = userCache.get(userId); 8 if (cached != null) { 9 return cached; 10 } 11 12 final MutableLiveData<User> data = new MutableLiveData<>(); 13 userCache.put(userId, data); 14 // this is still suboptimal but better than before. 15 // a complete implementation must also handle the error cases. 16 webservice.getUser(userId).enqueue(new Callback<User>() { 17 @Override 18 public void onResponse(Call<User> call, Response<User> response) { 19 data.setValue(response.body()); 20 } 21 }); 22 return data; 23 } 24 }
持久化数据
在咱们当前的实现中, 若是用户旋转了屏幕或者离开后再返回应用, 已存在UI将会当即可见, 由于repository从内存缓存中检索了数据. 可是若是用户离开了应用几个小时以后再返回呢, 在Android系统已经杀死了进程以后?
若是使用当前实现的话, 咱们须要从网络中再次拉取数据. 但这不只是个坏的用户体验, 并且浪费, 由于它使用了移动数据从新拉取相同的数据. 经过缓存网页请求, 你能够简单地修复这个问题. 但这又产生了新的问题. 若是相同的用户数据从其它类型的网页请求中展现出来呢? 好比, 拉取好友列表. 你的应用极可能会展现不一致的数据, 这充其量是一个混淆的用户体验. 举个例子, 相同用户的数据可能展现的不同, 由于好友列表请求和用户请求可能在不一样的时刻执行. 应用须要合并两个请求以免展现不一致的数据.
处理这些的正确方式是使用持久化模型. 这就是为何要使用Room持久化库.
Room是一个对象映射库, 使用最少的样板代码提供本地数据持久化. 在编译时, 根据计划验证每个查询, 由此, 坏的SQL请求展现编译期错误而非运行时失败. Room抽象了使用原生SQL表和查询的基本的实现细节. 它也容许观测数据库中数据的改变(包括集合和联接查询), 经过LiveData对象暴露这些改变. 此外, 它显式地定义了线程约束: 在主线程中访问存储.
备注: 若是应用已经使用了其它的诸如SQLite对象关系型映射的持久化解决方案, 你没必要用Room取代它们. 然而, 若是你在写新应用或者重构已有应用, 推荐使用Room持久化应用数据. 经过这种方式, 你可以充分利用库的抽象和查询验证能力.
要使用Room, 我须要定义本地schema. 首先, 用@Entity注解User类, 把它在数据库中标记为表:
1 @Entity 2 class User { 3 @PrimaryKey 4 private int id; 5 private String name; 6 private String lastName; 7 // getters and setters for fields 8 }
以后, 经过继承RoomDatabase建立数据库:
1 @Database(entities = {User.class}, version = 1) 2 public abstract class MyDatabase extends RoomDatabase { 3 }
你能够注意到MyDatabase是抽象类. Room会自动地提供它的实现.
如今咱们须要一种方式在用户数据插入数据库. 首先建立Data Access Object)(DAO):
1 @Dao 2 public interface UserDao { 3 @Insert(onConflict = REPLACE) 4 void save(User user); 5 @Query("SELECT * FROM user WHERE id = :userId") 6 LiveData<User> load(String userId); 7 }
以后从数据库类中引用DAO.
1 @Database(entities = {User.class}, version = 1) 2 public abstract class MyDatabase extends RoomDatabase { 3 public abstract UserDao userDao(); 4 }
请注意load()方法返回LiveData<User>. Room知道数据库修改的时间, 它会在数据发生改变时自动地通知全部依旧活跃的观测者. 由于在使用LiveData, 它会很是高效, 由于只有在至少一个活跃观测者时, 才会更新数据.
备注: Room基于数据表的修改来检测认证, 这意味着分发假阳性通知.
如今咱们可以修改UserRepository来跟Room数据源合做:
1 @Singleton 2 public class UserRepository { 3 private final Webservice webservice; 4 private final UserDao userDao; 5 private final Executor executor; 6 7 @Inject 8 public UserRepository(Webservice webservice, UserDao userDao, Executor executor) { 9 this.webservice = webservice; 10 this.userDao = userDao; 11 this.executor = executor; 12 } 13 14 public LiveData<User> getUser(String userId) { 15 refreshUser(userId); 16 // return a LiveData directly from the database. 17 return userDao.load(userId); 18 } 19 20 private void refreshUser(final String userId) { 21 executor.execute(() -> { 22 // running in a background thread 23 // check if user was fetched recently 24 boolean userExists = userDao.hasUser(FRESH_TIMEOUT); 25 if (!userExists) { 26 // refresh the data 27 Response response = webservice.getUser(userId).execute(); 28 // TODO check for error etc. 29 // Update the database.The LiveData will automatically refresh so 30 // we don't need to do anything else here besides updating the database 31 userDao.save(response.body()); 32 } 33 }); 34 } 35 }
请注意: 尽管咱们在UserRepository里面修改了数据来源, 但并不须要修改UserProfileViewModel和UserProfileFragment. 这就是抽象提供的灵活性. 这也对测试友好, 由于在测试UserProfileViewModel的时候, 你可以提供假的UserRepository.
如今代码完整了. 若是用户稍后回到相同的UI, 它们依然可以看到用户信息, 由于信息已经持久化了. 同时, 若是数据脏了的话, 咱们的Repository会在后台更新它们. 固然, 取决于你的用例, 若是持久化数据太老的话, 你也许选择不展现.
在一些用例中, 好比下拉刷新, 若是当前有网络操做正在进行, 在UI上向用户展现进度也很重要. 将UI操做与真实的数据分离开来是最佳实践, 由于UI操做会根据不一样的版本更新(好比, 若是咱们拉取好友列表, 用户可能经过触发LiveData<User>更新而再次拉取). 从UI的角度看, 有请求正在的进行是另外一个数据点, 类似于其它的碎片数据(好比User对象).
这个用例有两个通用解决方案:
单一真理之源
对于不一样的REST API, 返回相同的数据, 是很广泛的. 举个例子, 若是后台有另外一个终点返回好友列表, 相同的用户对象可能来自两个不一样的API终点, 也许以不一样的粒度. 若是UserRepository注定返回来自Webservice的请求的响应, UI可能会展现不一致的数据, 由于在后台的数据在两个请求之间能够发生改变. 这就是为何在UserRepository实现中, 网页服务回调将数据存储进数据库中. 以后, 数据库的改变会在LiveData对象上面触发回调.
在这个模型中, 数据库做为真理的单一之源, 应用的其它部分经过Repository访问它. 不管是否使用硬盘缓存, 推荐: repository指派数据源做为应用的单一真理之源.
测试
以前已经提到过: 分享的优点之一就是可测试性. 接下来看一下如何测试每个模块:
最终架构
下面的图表展现了推荐架构中全部的模块, 以及它们之间如何交互:
指导原则
编程是具备创造性的领域, 构建Anroid应用也不例外. 有不少种方式来解决一个问题. 这个问题能够是在多个activities/fragments之间通讯, 检索远程数据并将它做为离线模式在本地持久化, 或者其它多数应用遇到的常见场景.
尽管下面的推荐并不是强制, 但长久来看, 遵循它们会使得代码更多鲁棒, 可测试和可维护.
附录: 暴露网络状态
使用Resource类封闭数据和状态.
下面是实现示例:
1 //a generic class that describes a data with a status 2 public class Resource<T> { 3 @NonNull public final Status status; 4 @Nullable public final T data; 5 @Nullable public final String message; 6 private Resource(@NonNull Status status, @Nullable T data, @Nullable String message) { 7 this.status = status; 8 this.data = data; 9 this.message = message; 10 } 11 12 public static <T> Resource<T> success(@NonNull T data) { 13 return new Resource<>(SUCCESS, data, null); 14 } 15 16 public static <T> Resource<T> error(String msg, @Nullable T data) { 17 return new Resource<>(ERROR, data, msg); 18 } 19 20 public static <T> Resource<T> loading(@Nullable T data) { 21 return new Resource<>(LOADING, data, null); 22 } 23 }
由于边从网络加载数据边从硬盘展现是常见的用例, 建立一个能在不一样场合重用的帮助类NetworkBoundResource. 下面是NetworkBoundResource的决定树:
它从观测数据库资源开始. 在入口首次从数据库加载的时候, NetworkBoundResource检测结果是否足够好来分发或者它应该从网络拉取. 这两个场景有可能同时发生, 由于你能够一边展现缓存数据, 一边从网络更新该数据.
若是网络调用成功完成, 它将响应结果保存内数据库并从新初始化流. 若是网络请求失败, 直接分发错误结果.
备注: 在保存新的数据到硬盘以后, 咱们从数据库中从新初始化, 尽管, 一般咱们并不须要这样作, 由于数据库会分发这些变化. 另外一方面, 依赖数据库颁发这些改变依赖于很差的负做用, 由于若是数据没有发生改变, 数据库可以避免分发这些改变. 咱们也不想分发产生自网络的结果, 由于这违反了单一真理之源原则(也许有数据库里有触发器, 会改变保存的值). 咱们也不想要在没有新数据的状况下分发SUCCESS, 由于它会向客户端发送错误的信息.
如下是NetworkBoundResource为子类提供的公共API:
1 // ResultType: Type for the Resource data 2 // RequestType: Type for the API response 3 public abstract class NetworkBoundResource<ResultType, RequestType> { 4 // Called to save the result of the API response into the database 5 @WorkerThread 6 protected abstract void saveCallResult(@NonNull RequestType item); 7 8 // Called with the data in the database to decide whether it should be 9 // fetched from the network. 10 @MainThread 11 protected abstract boolean shouldFetch(@Nullable ResultType data); 12 13 // Called to get the cached data from the database 14 @NonNull @MainThread 15 protected abstract LiveData<ResultType> loadFromDb(); 16 17 // Called to create the API call. 18 @NonNull @MainThread 19 protected abstract LiveData<ApiResponse<RequestType>> createCall(); 20 21 // Called when the fetch fails. The child class may want to reset components 22 // like rate limiter. 23 @MainThread 24 protected void onFetchFailed() { 25 } 26 27 // returns a LiveData that represents the resource, implemented 28 // in the base class. 29 public final LiveData<Resource<ResultType>> getAsLiveData(); 30 }
注意, 以上类定义了两个类型参数(ResultType, RequestType), 由于API返回的数据类型也许并不匹配本地使用的数据类型.
也要注意, 以上代码使用了ApiResponse用于网络请求. ApiResponse是个简单的Retrofit2.Call包裹类, 将响应转变成LiveData.
如下是NetworkBondResource实现的余下部分:
1 public abstract class NetworkBoundResource<ResultType, RequestType> { 2 private final MediatorLiveData<Resource<ResultType>> result = new MediatorLiveData<>(); 3 4 @MainThread 5 NetworkBoundResource() { 6 result.setValue(Resource.loading(null)); 7 LiveData<ResultType> dbSource = loadFromDb(); 8 result.addSource(dbSource, data -> { 9 result.removeSource(dbSource); 10 if (shouldFetch(data)) { 11 fetchFromNetwork(dbSource); 12 } else { 13 result.addSource(dbSource, 14 newData -> result.setValue(Resource.success(newData))); 15 } 16 }); 17 } 18 19 private void fetchFromNetwork(final LiveData<ResultType> dbSource) { 20 LiveData<ApiResponse<RequestType>> apiResponse = createCall(); 21 // we re-attach dbSource as a new source, 22 // it will dispatch its latest value quickly 23 result.addSource(dbSource, 24 newData -> result.setValue(Resource.loading(newData))); 25 result.addSource(apiResponse, response -> { 26 result.removeSource(apiResponse); 27 result.removeSource(dbSource); 28 //noinspection ConstantConditions 29 if (response.isSuccessful()) { 30 saveResultAndReInit(response); 31 } else { 32 onFetchFailed(); 33 result.addSource(dbSource, 34 newData -> result.setValue( 35 Resource.error(response.errorMessage, newData))); 36 } 37 }); 38 } 39 40 @MainThread 41 private void saveResultAndReInit(ApiResponse<RequestType> response) { 42 new AsyncTask<Void, Void, Void>() { 43 44 @Override 45 protected Void doInBackground(Void... voids) { 46 saveCallResult(response.body); 47 return null; 48 } 49 50 @Override 51 protected void onPostExecute(Void aVoid) { 52 // we specially request a new live data, 53 // otherwise we will get immediately last cached value, 54 // which may not be updated with latest results received from network. 55 result.addSource(loadFromDb(), 56 newData -> result.setValue(Resource.success(newData))); 57 } 58 }.execute(); 59 } 60 61 public final LiveData<Resource<ResultType>> getAsLiveData() { 62 return result; 63 } 64 }
如今, 咱们可以经过在repository中绑定User实现来使用NetworkBoundResource写硬盘和网络.
1 class UserRepository { 2 Webservice webservice; 3 UserDao userDao; 4 5 public LiveData<Resource<User>> loadUser(final String userId) { 6 return new NetworkBoundResource<User,User>() { 7 @Override 8 protected void saveCallResult(@NonNull User item) { 9 userDao.insert(item); 10 } 11 12 @Override 13 protected boolean shouldFetch(@Nullable User data) { 14 return rateLimiter.canFetch(userId) && (data == null || !isFresh(data)); 15 } 16 17 @NonNull @Override 18 protected LiveData<User> loadFromDb() { 19 return userDao.load(userId); 20 } 21 22 @NonNull @Override 23 protected LiveData<ApiResponse<User>> createCall() { 24 return webservice.getUser(userId); 25 } 26 }.getAsLiveData(); 27 } 28 }