Fork me on GitHub

Android应用架构简介

 

对于经过过构建app的Android开发人员来说, 现在是时候了解一下构建鲁棒, 质量高的应用的最佳实践和推荐架构了.

这篇文章假设读者对Android framework比较熟悉.

OK, let's begin!

 

App开发人员面临的常见问题

 

传统的桌面开发, 在大多数情况下, 拥有一个来自Launcher快捷键的单独入口点, 并在独立的整体进程中运行. 而Android应用则拥有更多复杂的结构. 典型的Android应用由多个应用构件组成, 包括Activities, Fragments, Services, ContentProviders和BroadcastReceivers.

大多数这些应用构件声明在AndroidManifest文件中, 该文件被Android系统使用以将应用整合进全面的用户体验中. 尽管, 如先前所言, 传统的桌面应用作为一个完整的进程运行, 而正确书写的Android应用需要更多的灵活性, 因为用户通过频繁地切换流和任务, 在设备上不同的应用间编写不同的路径.

比如, 当你在最喜欢的社交网络应用上分享照片时, 想想会发生什么吧. 应用触发了一个相机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, 我们的数据模型需要持有两个数据元素.

  • User ID: 用户标识符. 最好使用fragment的arguments将这些信息传给fragment. 如果Android系统销毁了你的进程, 这个信息将会被保存, 所以在下次应用重启时, id是可用的.
  • User object: 持有用户数据的POJO.

我们会基于ViewModel类创建UserProfileViewModel来保持信息.
ViewModel为特定的UI构件, 诸如Activity/Fragment, 提供数据, 并处理与数据处理的业务部分的通讯, 诸如调用其它构件加载数据或者提交用户修改. ViewModel并不知晓视图, 且并不受诸如由于屏幕旋转导致的Activity重建等配置改变的影响.

现在我们有三个文件:

  • user_profile.xml: 为屏幕定义的UI.
  • UserProfileViewModel.java: 为UI准备数据的类.
  • UserProfileFragment.java: 在ViewModel中展示数据和对用户交互反馈的UI控制器.

下面是起始实现(为简单起见, 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, 这将非常消耗资源.

你可以使用两种方式解决这个问题:

  • Dependency Injection: 依赖注入允许类在不构建实例的前提下定义他们的依赖. 在运行时, 其它的类负责提供这些依赖. 推荐使用Google的Dagger 2在Android应用中实现依赖注入. Dagger 2通过遍历依赖树自动地构建对象, 并提供对依赖的编译期保证.
  • Service Locator: 服务定位器提供一个注册, 使得类能够获取它们的依赖而非构建它们. 实现依赖注入(DI)相对容易, 所以如果你不熟悉DI, 那就使用Service Locator吧.

这些模式允许你衡量自己的代码, 因为他们为在没有重复代码和添加复杂性的的前提下管理依赖提供了清晰的模式. 两者都允许将实现转换成测试; 这是使用两者的主要好处.

 

连接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对象).

这个用例有两个通用解决方案:

  • 修改getUser返回LiveData, 使它包含网络操作的状态.
  • 在Repository类中提供另一个公共方法, 能够刷新用户状态. 如果你只想在UI中展示网络状态作为显式用户操作(像下拉刷新)的回应的话, 这个选项更好.

单一真理之源

对于不同的REST API, 返回相同的数据, 是很普遍的. 举个例子, 如果后台有另一个终点返回好友列表, 相同的用户对象可能来自两个不同的API终点, 也许以不同的粒度. 如果UserRepository注定返回来自Webservice的请求的响应, UI可能会展示不一致的数据, 因为在后台的数据在两个请求之间可以发生改变. 这就是为什么在UserRepository实现中, 网页服务回调将数据存储进数据库中. 之后, 数据库的改变会在LiveData对象上面触发回调.

在这个模型中, 数据库作为真理的单一之源, 应用的其它部分通过Repository访问它. 无论是否使用硬盘缓存, 推荐: repository指派数据源作为应用的单一真理之源.

测试

之前已经提到过: 分享的优势之一就是可测试性. 接下来看一下如何测试每一个模块:

  • 用户接口和交互: 这是唯一的一次需要使用Android UI Instrumentation测试. 测试UI代码的最好方式是创建Espresso测试. 可以创建Fragment, 然后提供一个模拟的ViewModel. 因为Fragment只跟ViewModel交流, 模拟ViewModel对于完全测试UI是很充分的.
  • ViewModel: ViewModel可能通过JUnit来测试. 需要模拟唯一的UserRepository.
  • UserRepository: 也可以通过JUnit测试UserRepository. 只不过需要模拟Webservice和DAO. UserRepository调用正确的网页服务, 把结果保存到数据库, 并且, 如果数据缓存过并且是最新的, 也不会发出不必要的请求. 因为Webservice和UserDao都是接口, 你能够模拟它们或者为更复杂的测试用例创建伪实现.
  • UserDao: 测试DAO类的推荐途径是使用instrumentation测试. 因为instrumentation测试并不要求UI, 依然运行地很快. 对于每一个测试, 可以创建内存内数据库以保障测试并不会拥有任何负作用(例如改变硬盘里面数据库文件). Room也允许指定数据库实现, 所以可以通过给它提供SupportSQLiteOpenHelper的JUnit实现来测试它. 这个方式通常并不值得推荐, 因为不同设备上的SQLite版本也许和虚拟机上的SQLite版本不同.
  • Webservice: 进行独立于外部世界的测试很重要. 所以, 即使是Webservice测试也应该避免向后台进行网络请求. 有大量的库可以做到这些. 举个例子, MockWebServer是个很好的包, 帮助你创建伪的本地服务器.
  • Testing Artifacts结构组件提供了maven artifact来控件后台线程. 在android.arch.core:core-testing内部, 有两个JUnit规则:
  1. InstantTaskExecutorRule: 这个规则使用强制结构组件在调用者线程上即时执行任何后台操作.
  2. CountingTaskExecutorRule: 这个规则在instrumentation测试中使用, 以等待结构组件的后台操作, 或者作为空闲资源连接给Espresso.

最终架构

下面的图表展示了推荐架构中所有的模块, 以及它们之间如何交互:

指导原则

编程是具有创造性的领域, 构建Anroid应用也不例外. 有很多种方式来解决一个问题. 这个问题可以是在多个activities/fragments之间通信, 检索远程数据并将它作为离线模式在本地持久化, 或者其它多数应用遇到的常见场景.

尽管下面的推荐并非强制, 但长久来看, 遵循它们会使得代码更多鲁棒, 可测试和可维护.

  • 在AndroidManifest中定义的入口--activities, services, broadcast receivers--并不是数据源. 相反, 它们应该只是协同于入口相关数据的子集. 因为每一个应用组件都活得相当短, 依赖于用户与设备的交互和运行时当前的完全健康状况, 你不会想任何入口成为数据源的.
  • 在应用的不同模块间毫无怜悯地创建定义良好的责任边界. 比如, 不要把网络加载数据的代码分散到不同的类和包. 而且也不要将不相关的责任填在相同的类里面, 例如数据缓存和数据绑定.
  • 模块间尽可能少地暴露细节. 不要尝试去创建暴露其它模块实现细节的快捷入口. 短期看可能节省一些时间, 但是随着代码的进化, 你会多付好几倍的技术债.
  • 在定义模块间交互的时候, 考虑如何单独测试每个模块. 比如, 拥有定义良好的网络拉取数据的API会使得测试在本地数据库持久化数据的模块更易测试. 如果你在一个地方混合了两个模块的逻辑, 或者将网络相关代码弄得整个项目都是, 测试将会很难.
  • 应用的核心是应用的突出点. 不要花时间重复造轮子或者一遍又一遍地写模板代码. 集中精神力量于应用独一无二的地方, 让Android结构组件和其它的推荐库去处理模板代码.
  • 尽可能将相关性大和新鲜的数据持久化, 这样你在应用在离线模式下也是可用的. 你可能享受稳定的高速的连接, 用户可未必.
  • Repository应用指定单一真理数据源. 无论任何时候应用需要访问数据, 都应该从单一真理数据源产生.

附录: 暴露网络状态

使用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 }
posted on 2018-07-12 16:36  SilentKnight  阅读(1405)  评论(0编辑  收藏  举报