App开发架构指南(谷歌官方文档译文)(转)

文章转自http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2017/0523/7963.html

app开发者面临的常见问题

跟传统的桌面应用开发不同,Android app的架构要复杂得多。一个典型的Android app是由多个app组件构成的,包括activity,Fragment,service,content provider以及broadcast receiver。而传统的桌面应用往往在一个庞大的单一的进程中就完成了。

大多数的app组件都声明在app manifest中,Android OS用它来决定如何将你的app与设备整合形成统一的用户体验。虽然就如刚说的,桌面app只运行一个进程,但是一个优秀的Android app却需要更加灵活,因为用户操作在不同app之间,不断的切换流程和任务。

比如,当你要在自己最喜欢的社交网络app中分享一张照片的时候,你可以想象一下会发生什么。app触发一个camera intent,然后Android OS启动一个camera app来处理这一动作。此时用户已经离开了社交网络的app,但是用户的操作体验却是无缝对接的。而 camera app反过来也可能触发另一个intent,比如启动一个文件选择器,这可能会再次打开另一个app。最后用户回到社交网络app并分享照片。在这期间的任意时刻用户都可被电话打断,打完电话之后继续回来分享照片。

在Android中,这种app并行操作的行为是很常见的,因此你的app必须正确处理这些流程。还要记住移动设备的资源是有限的,因此任何时候操作系统都有可能杀死某些app,为新运行的app腾出空间。

总的来说就是,你的app组件可能是单独启动并且是无序的,而且在任何时候都有可能被系统或者用户销毁。因为app组件生命的短暂性以及生命周期的不可控制性,任何数据都不应该把存放在app组件中,同时app组件之间也不应该相互依赖。

通用的架构准则

如果app组件不能存放数据和状态,那么app还是可架构的吗?

最重要的一个原则就是尽量在app中做到separation of concerns(关注点分离)。常见的错误就是把所有代码都写在Activity或者Fragment中。任何跟UI和系统交互无关的事情都不应该放在这些类当中。尽可能让它们保持简单轻量可以避免很多生命周期方面的问题。别忘了能并不拥有这些类,它们只是连接app和操作系统的桥梁。根据用户的操作和其它因素,比如低内存,Android OS可能在任何时候销毁它们。为了提供可靠的用户体验,最好把对它们的依赖最小化。

第二个很重要的准则是用model驱动UI,最好是持久化的model。之所以要持久化是基于两个原因:如果OS销毁app释放资源,用户数据不会丢失;当网络很差或者断网的时候app可以继续工作。Model是负责app数据处理的组件。它们不依赖于View或者app 组件(Activity,Fragment等),因此它们不会受那些组件的生命周期的影响。保持UI代码的简单,于业务逻辑分离可以让它更易管理。

app架构推荐

在这一小节中,我们将通过一个用例演示如何使用Architecture Component构建一个app。

注:没有一种适合所有场景的app编写方式。也就是说,这里推荐的架构适合作为大多数用户案例的开端。但是如果你已经有了一种好的架构,没有必要再去修改。

假设我们在创建一个显示用户简介的UI。用户信息取自我们自己的私有的后端REST API。

创建用户界面

UI由UserProfileFragment.java以及相应的布局文件user_profile_layout.xml组成。

要驱动UI,我们的data model需要持有两个数据元素。

User ID: 用户的身份识别。最好使用fragment argument来传递这个数据。如果OS杀死了你的进程,这个数据可以被保存下来,所以app再次启动的时候id仍是可用的。

User object: 一个持有用户信息数据的POJO对象。

我们将创建一个继承ViewModel类的UserProfileViewModel来保存这一信息。

一个ViewModel为特定的UI组件提供数据,比如fragment 或者 activity,并负责和数据处理的业务逻辑部分通信,比如调用其它组件加载数据或者转发用户的修改。ViewModel并不知道View的存在,也不会被configuration change影响。

现在我们有了三个文件。

user_profile.xml: 定义页面的UI

UserProfileViewModel.java: 为UI准备数据的类

UserProfileFragment.java: 显示ViewModel中的数据与响应用户交互的控制器

下面我们开始实现(为简单起见,省略了布局文件):

<pre class="brush:js;toolbar:false prettyprint linenums prettyprinted" style="box-sizing: border-box; overflow: auto; font-family: Menlo, "Bitstream Vera Sans Mono", "DejaVu Sans Mono", Monaco, Consolas, monospace; font-size: 14px; display: block; padding: 1.5em; margin: 0px 0px 20px; line-height: 1.42857; word-break: break-all; word-wrap: break-word; color: rgb(51, 51, 51); background: rgb(47, 54, 64); border: 0px !important; border-radius: 0px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">

  1. public class UserProfileViewModel extends ViewModel {

  2. private String userId;

  3. private User user;

  4. public void init(String userId) {

  5. this.userId = userId;

  6. }

  7. public User getUser() {

  8. return user;

  9. }

  10. }

</pre>

<pre class="brush:js;toolbar:false prettyprint linenums prettyprinted" style="box-sizing: border-box; overflow: auto; font-family: Menlo, "Bitstream Vera Sans Mono", "DejaVu Sans Mono", Monaco, Consolas, monospace; font-size: 14px; display: block; padding: 1.5em; margin: 0px 0px 20px; line-height: 1.42857; word-break: break-all; word-wrap: break-word; color: rgb(51, 51, 51); background: rgb(47, 54, 64); border: 0px !important; border-radius: 0px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">

  1. public class UserProfileFragment extends LifecycleFragment {

  2. private static final String UID_KEY = "uid";

  3. private UserProfileViewModel viewModel;

  4. @Override

  5. public void onActivityCreated(@Nullable Bundle savedInstanceState) {

  6. super.onActivityCreated(savedInstanceState);

  7. String userId = getArguments().getString(UID_KEY);

  8. viewModel = ViewModelProviders.of(this).get(UserProfileViewModel.class);

  9. viewModel.init(userId);

  10. }

  11. @Override

  12. public View onCreateView(LayoutInflater inflater,

  13. @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {

  14. return inflater.inflate(R.layout.user_profile, container, false);

  15. }

  16. }

</pre>

注:上面的例子中继承的是LifecycleFragment而不是Fragment类。等Architecture Component中的lifecycles API稳定之后,Android Support Library中的Fragment类也将实现LifecycleOwner

现在我们有了这些代码模块,如何连接它们呢?毕竟当ViewModel的user成员设置之后,我们还需要把它显示到界面上。这就要用到LiveData了。

LiveData是一个可观察的数据持有者。 无需明确在它与app组件之间创建依赖就可以观察LiveData对象的变化。LiveData还考虑了app组件(activities, fragments, services)的生命周期状态,做了防止对象泄漏的事情。

注:如果你已经在使用RxJava或者Agera这样的库,你可以继续使用它们,而不使用LiveData。但是使用它们的时候要确保正确的处理生命周期的问题,与之相关的LifecycleOwner stopped的时候数据流要停止,LifecycleOwner destroyed的时候数据流也要销毁。你也可以使用android.arch.lifecycle:reactivestreams让LiveData和其它的响应式数据流库一起使用(比如, RxJava2)。

现在我们把UserProfileViewModel中的User成员替换成LiveData,这样当数据发生变化的时候fragment就会接到通知。LiveData的妙处在于它是有生命周期意识的,当它不再被需要的时候会自动清理引用。

<pre class="brush:js;toolbar:false prettyprint linenums prettyprinted" style="box-sizing: border-box; overflow: auto; font-family: Menlo, "Bitstream Vera Sans Mono", "DejaVu Sans Mono", Monaco, Consolas, monospace; font-size: 14px; display: block; padding: 1.5em; margin: 0px 0px 20px; line-height: 1.42857; word-break: break-all; word-wrap: break-word; color: rgb(51, 51, 51); background: rgb(47, 54, 64); border: 0px !important; border-radius: 0px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">

  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. }

</pre>

现在我们修改UserProfileFragment,让它观察数据并更新UI。

<pre class="prettyprint linenums prettyprinted" style="box-sizing: border-box; overflow: auto; font-family: Menlo, "Bitstream Vera Sans Mono", "DejaVu Sans Mono", Monaco, Consolas, monospace; font-size: 14px; display: block; padding: 1.5em; margin: 0px 0px 20px; line-height: 1.42857; word-break: break-all; word-wrap: break-word; color: rgb(51, 51, 51); background: rgb(47, 54, 64); border: 0px !important; border-radius: 0px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">

  1. @Override
  2. public void onActivityCreated(@Nullable Bundle savedInstanceState) {
  3. super.onActivityCreated(savedInstanceState);
  4. viewModel.getUser().observe(this, user -> {
  5. // update UI
  6. });
  7. }

</pre>

每当User数据更新的时候 onChanged 回调将被触发,然后刷新UI。

如果你熟悉其它library的observable callback的用法,你会意识到我们不需要重写fragment的onStop()方法停止对数据的观察。因为LiveData是有生命周期意识的,也就是说除非fragment处于活动状态,否则callback不会触发。LiveData还可以在fragmentonDestroy()的时候自动移除observer。

对我们也没有做任何特殊的操作来处理 configuration changes(比如旋转屏幕)。ViewModel可以在configuration change的时候自动保存下来,一旦新的fragment进入生命周期,它将收到相同的ViewModel实例,并且携带当前数据的callback将立即被调用。这就是为什么ViewModel不应该直接引用任何View,它们游离在View的生命周期之外。参见ViewModel的生命周期

获取数据

现在我们把ViewModel和fragment联系了起来,但是ViewModel该如何获取数据呢?在我们的例子中,假设后端提供一个REST API,我们使用Retrofit从后端提取数据。你也可以使用任何其它的library来达到相同的目的。

下面是和后端交互的retrofit Webservice:

<pre class="brush:js;toolbar:false prettyprint linenums prettyprinted" style="box-sizing: border-box; overflow: auto; font-family: Menlo, "Bitstream Vera Sans Mono", "DejaVu Sans Mono", Monaco, Consolas, monospace; font-size: 14px; display: block; padding: 1.5em; margin: 0px 0px 20px; line-height: 1.42857; word-break: break-all; word-wrap: break-word; color: rgb(51, 51, 51); background: rgb(47, 54, 64); border: 0px !important; border-radius: 0px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">

  1. public interface Webservice {
  2. /**
    • @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
  3. */
  4. @GET("/users/{user}")
  5. Call<User> getUser(@Path("user") String userId);
  6. }

</pre>

ViewModel的一个简单的实现方式是直接调用Webservice获取数据,然后把它赋值给User对象。虽然这样可行,但是随着app的增大会变得难以维护。ViewModel的职责过多也违背了前面提到的关注点分离(separation of concerns)原则。另外,ViewModel的有效时间是和ActivityFragment的生命周期绑定的,因此当它的生命周期结束便丢失所有数据是一种不好的用户体验。相反,我们的ViewModel将把这个工作代理给Repository模块。

Repository模块负责处理数据方面的操作。它们为app提供一个简洁的API。它们知道从哪里得到数据以及数据更新的时候调用什么API。你可以把它们看成是不同数据源(persistent model, web service, cache, 等等)之间的媒介。

下面的UserRepository类使用了WebService来获取用户数据。

<pre class="brush:js;toolbar:false prettyprint linenums prettyprinted" style="box-sizing: border-box; overflow: auto; font-family: Menlo, "Bitstream Vera Sans Mono", "DejaVu Sans Mono", Monaco, Consolas, monospace; font-size: 14px; display: block; padding: 1.5em; margin: 0px 0px 20px; line-height: 1.42857; word-break: break-all; word-wrap: break-word; color: rgb(51, 51, 51); background: rgb(47, 54, 64); border: 0px !important; border-radius: 0px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">

  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. }

</pre>

虽然repository模块看起来没什么必要,但它其实演扮演着重要的角色;它把数据源从app中抽象出来。现在我们的ViewModel并不知道数据是由Webservice提供的,意味着有必要的话可以替换成其它的实现方式。

注:为简单起见我们省略了网络错误出现的情况。实现了暴露网络错误和加载状态的版本见下面的Addendum: exposing network status

管理不同组件间的依赖:

前面的UserRepository类需要Webservice的实例才能完成它的工作。可以直接创建它就是了,但是为此我们还需要知道Webservice所依赖的东西才能构建它。这显著的增减了代码的复杂度和偶合度(比如,每个需要Webservice实例的类都需要知道如何用它的依赖去构建它)。另外,UserRepository很可能不是唯一需要Webservice的类。如果每个类都创建一个新的WebService,就变得很重了。

有两种模式可以解决这个问题:

依赖注入: 依赖注入允许类在无需构造依赖的情况下定义自己的依赖对象。在运行时由另一个类来负责提供这些依赖。在Android app中我们推荐使用谷歌的Dagger 2来实现依赖注入。Dagger 2 通过遍历依赖树自动构建对象,并提供编译时的依赖。

Service Locator:Service Locator 提供一个registry,类可以从这里得到它们的依赖而不是构建它们。相对依赖注入来说要简单些,所以如果你对依赖注入不熟悉,可以使用 Service Locator 。

这些模式允许你扩展自己的代码,因为它们提供了清晰的模式来管理依赖,而不是不断的重复代码。两者均支持替换成mock依赖来测试,这也是使用它们主要优势之一。

在这个例子中,我们将使用 Dagger 2 来管理依赖。

连接ViewModel和repository

现在我们修改UserProfileViewModel以使用repository。

<pre class="brush:js;toolbar:false prettyprint linenums prettyprinted" style="box-sizing: border-box; overflow: auto; font-family: Menlo, "Bitstream Vera Sans Mono", "DejaVu Sans Mono", Monaco, Consolas, monospace; font-size: 14px; display: block; padding: 1.5em; margin: 0px 0px 20px; line-height: 1.42857; word-break: break-all; word-wrap: break-word; color: rgb(51, 51, 51); background: rgb(47, 54, 64); border: 0px !important; border-radius: 0px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">

  1. public class UserProfileViewModel extends ViewModel {

  2. private LiveData<User> user;

  3. private UserRepository userRepo;

  4. @Inject // UserRepository parameter is provided by Dagger 2

  5. public UserProfileViewModel(UserRepository userRepo) {

  6. this.userRepo = userRepo;

  7. }

  8. public void init(String userId) {

  9. if (this.user != null) {

  10. // ViewModel is created per Fragment so

  11. // we know the userId won't change

  12. return;

  13. }

  14. user = userRepo.getUser(userId);

  15. }

  16. public LiveData<User> getUser() {

  17. return this.user;

  18. }

  19. }

</pre>

缓存数据

上面的repository对抽象web service调用是很好的,但是因为它只依赖于一个数据源,并不是非常实用。

UserRepository的问题在于当获取完数据之后,它并没有把数据保存下来。如果用户离开UserProfileFragment然后在回来,app会重新获取数据。这是很不好的,原因有二:1.浪费了带宽资源,2.用户被迫等待新的查询完成。为了解决这个问题,我们向UserRepository中添加了一个新的数据源,它将把User对象缓存到内存中。

<pre class="brush:js;toolbar:false prettyprint linenums prettyprinted" style="box-sizing: border-box; overflow: auto; font-family: Menlo, "Bitstream Vera Sans Mono", "DejaVu Sans Mono", Monaco, Consolas, monospace; font-size: 14px; display: block; padding: 1.5em; margin: 0px 0px 20px; line-height: 1.42857; word-break: break-all; word-wrap: break-word; color: rgb(51, 51, 51); background: rgb(47, 54, 64); border: 0px !important; border-radius: 0px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">

  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. final MutableLiveData<User> data = new MutableLiveData<>();

  12. userCache.put(userId, data);

  13. // this is still suboptimal but better than before.

  14. // a complete implementation must also handle the error cases.

  15. webservice.getUser(userId).enqueue(new Callback<User>() {

  16. @Override

  17. public void onResponse(Call<User> call, Response<User> response) {

  18. data.setValue(response.body());

  19. }

  20. });

  21. return data;

  22. }

  23. }

</pre>

持久化数据

目前的实现中,如果用户旋转屏幕或者是离开之后再次回到app,UI将立即可见,因为repository是从常驻内存的缓存中获取的数据。但是如果用户离开了app,几个小时之后再回来时进程已经被杀死了怎么办呢?

以目前的实现来看,我们需要再次从网络获取数据。这不仅仅是糟糕的用户体验,还是一种浪费,因为它需要花费移动流量获取相同的数据。你可以直接缓存web请求,但是这又产生了新的问题。如果同样的user数据来自于另一个类型的请求呢(比如获取一个朋友的列表)?那样的话你的app很可能会显示不一致的数据,这是一种困惑的用户体验。例如,因为朋友列表请求与用户请求可能在不同的时间执行,同一用户的数据可能会不一致。你的app需要融合它们以避免数据出现不一致。

处理这个问题的正确方式是使用持久化的model。持久化库Room就是为此而生。

Room是一个对象关系映射库,以最少的代码提供本地数据持久化功能。它在编译时验证每个查询,所以损坏的SQL查询只会导致编译时错误而不是运行时崩溃。Room抽象了部分SQL查询与表的相关操作的底层细节。它还可以让你通过一个LiveData对象监听到数据库数据的变化。另外,它还明确定义了线程约束,解决了诸如从主线程获取存储这样的常见的问题。

注:如果熟悉其它的持久化方案比如SQLite ORM或者是一个不同的数据库,如Realm,你不需要把它替换成Room,除非Room的特性对你的用例而言更加重要。

要使用Room,我们需要定义本地的schema。首先使用@Entity注解User类,将它标记为数据库中的一张表。

<pre class="brush:js;toolbar:false prettyprint linenums prettyprinted" style="box-sizing: border-box; overflow: auto; font-family: Menlo, "Bitstream Vera Sans Mono", "DejaVu Sans Mono", Monaco, Consolas, monospace; font-size: 14px; display: block; padding: 1.5em; margin: 0px 0px 20px; line-height: 1.42857; word-break: break-all; word-wrap: break-word; color: rgb(51, 51, 51); background: rgb(47, 54, 64); border: 0px !important; border-radius: 0px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">

  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. }

</pre>

然后通过继承RoomDatabase创建一个database类:

<pre class="brush:js;toolbar:false prettyprint linenums prettyprinted" style="box-sizing: border-box; overflow: auto; font-family: Menlo, "Bitstream Vera Sans Mono", "DejaVu Sans Mono", Monaco, Consolas, monospace; font-size: 14px; display: block; padding: 1.5em; margin: 0px 0px 20px; line-height: 1.42857; word-break: break-all; word-wrap: break-word; color: rgb(51, 51, 51); background: rgb(47, 54, 64); border: 0px !important; border-radius: 0px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">

  1. @Database(entities = {User.class}, version = 1)
  2. public abstract class MyDatabase extends RoomDatabase {
  3. }

</pre>

注意MyDatabase是抽象类。Room根据它自动提供一个实现。详细情况参见Room文档。

现在我们需要一个向数据库插入数据的方法。为此创建一个data access object (DAO)

<pre class="brush:js;toolbar:false prettyprint linenums prettyprinted" style="box-sizing: border-box; overflow: auto; font-family: Menlo, "Bitstream Vera Sans Mono", "DejaVu Sans Mono", Monaco, Consolas, monospace; font-size: 14px; display: block; padding: 1.5em; margin: 0px 0px 20px; line-height: 1.42857; word-break: break-all; word-wrap: break-word; color: rgb(51, 51, 51); background: rgb(47, 54, 64); border: 0px !important; border-radius: 0px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">

  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. }

</pre>

然后,在database类中引用DAO。

<pre class="brush:js;toolbar:false prettyprint linenums prettyprinted" style="box-sizing: border-box; overflow: auto; font-family: Menlo, "Bitstream Vera Sans Mono", "DejaVu Sans Mono", Monaco, Consolas, monospace; font-size: 14px; display: block; padding: 1.5em; margin: 0px 0px 20px; line-height: 1.42857; word-break: break-all; word-wrap: break-word; color: rgb(51, 51, 51); background: rgb(47, 54, 64); border: 0px !important; border-radius: 0px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">

  1. @Database(entities = {User.class}, version = 1)
  2. public abstract class MyDatabase extends RoomDatabase {
  3. public abstract UserDao userDao();
  4. }

</pre>

注意load方法返回的是LiveData。Room知道database什么时候被修改过,当数据变化的时候,它将自动通知所有处于活动状态的observer。

注:对于alpha 1 版本,Room是根据表的修改检查验证,因此可能会发送错误的信号。

现在我们可以修改UserRepository以和Room协同工作。

<pre class="brush:js;toolbar:false prettyprint linenums prettyprinted" style="box-sizing: border-box; overflow: auto; font-family: Menlo, "Bitstream Vera Sans Mono", "DejaVu Sans Mono", Monaco, Consolas, monospace; font-size: 14px; display: block; padding: 1.5em; margin: 0px 0px 20px; line-height: 1.42857; word-break: break-all; word-wrap: break-word; color: rgb(51, 51, 51); background: rgb(47, 54, 64); border: 0px !important; border-radius: 0px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">

  1. @Singleton

  2. public class UserRepository {

  3. private final Webservice webservice;

  4. private final UserDao userDao;

  5. private final Executor executor;

  6. @Inject

  7. public UserRepository(Webservice webservice, UserDao userDao, Executor executor) {

  8. this.webservice = webservice;

  9. this.userDao = userDao;

  10. this.executor = executor;

  11. }

  12. public LiveData<User> getUser(String userId) {

  13. refreshUser(userId);

  14. // return a LiveData directly from the database.

  15. return userDao.load(userId);

  16. }

  17. private void refreshUser(final String userId) {

  18. executor.execute(() -> {

  19. // running in a background thread

  20. // check if user was fetched recently

  21. boolean userExists = userDao.hasUser(FRESH_TIMEOUT);

  22. if (!userExists) {

  23. // refresh the data

  24. Response response = webservice.getUser(userId).execute();

  25. // TODO check for error etc.

  26. // Update the database.The LiveData will automatically refresh so

  27. // we don't need to do anything else here besides updating the database

  28. userDao.save(response.body());

  29. }

  30. });

  31. }

  32. }

</pre>

虽然我们在UserRepository中改变了数据来源,但是我们不需要修改UserProfileViewModel或者UserProfileFragment。这就是抽象带来的灵活性。这对测试同样有好处,因为你可以提供一个假的UserRepository来测试UserProfileViewModel。

现在我们的代码就完成了。如果用户稍后回到相同的界面,将立即看到用户信息,因为这些信息做了持久化。同时,如果数据过于陈旧,repository将在后台更新数据。当然,这取决于你的案例,你可能会喜欢在数据太老的情况下不显示持久化的数据。

在某些情况下,比如下拉刷新,在界面上显示当前是否正在进行网络操作是很重要的。把UI操作和和实际数据分离是一种很好的实践,因为它可能因为各种原因而更新(比如,如果我们获取一个朋友的列表,同一用户可能被再次获取,从而触发一个 LiveData<User> update)。

这个问题有两种常用的解决办法:

  1. 把getUser修改成返回包含了网络操作状态的LiveData。附: 暴露网络状态小节中提供了一个实现了的例子。

  2. 在repository类中另外提供一个可以返回User刷新状态的公共的函数。如果只是为了明确的响应用户操作而在界面上显示网络状态((比如 pull-to-refresh).),这种方式更好些。

单一数据源(Single source of truth)

不同的后端REST API返回相同的数据是很常见的事情。比如,如果有一个另外的后端地址返回一个朋友的列表,那么相同的User对象就有可能来自两个不同的API地址,也许连内容详细程度都不一样。如果UserRepository直接返回 Webservice 请求 的响应,我们的UI就有可能显示不一致,因为服务端的两个请求之间数据是有区别的。这就是为什么在UserRepository的实现中,web service 的回调只是把数据保存在数据库中。然后数据库中的变化将触发LiveData对象的callback。

在这个模型中,database就是这里的单一数据源(Single source of truth),其它部分都是通过repository获取它。不管你是否是使用磁盘缓存,我们都推荐你的repository指明一个可以作为single source of truth数据源供app其它部分使用。

最终架构

下面的图标显示了我们所推荐的架构的所有模块以及它们之间是如何交互的:

final-architecture.png

指导原则

  • 编程是一种创造性的劳动,开发Android app也不例外。同一问题有许多种解决办法,不管是activity或者fragment之间的数据交互,还是获取远程数据并缓存到本地,还是一个有一定规模的app可能遇到的其它常见问题。

  • 虽然下面的建议不是强制性的,但根据我们以往的经验,从长远来看,遵守它们可以让你的代码更加健壮,更加易于测试和维护。

  • manifest定义的入口-activity, service, broadcast receiver..等等,都不是数据源,它们只应该和与该入口相关的数据协作。因为每个app组件的生命都非常短暂,取决于用户的行为以及设备运行时的状况,你肯定不会希望把它们作为数据源。

  • app不同模块之间要有明确的责任边界。比如,不要把加载网络数据的代码分散到不同的类或者包中。同样,也不要把不相关的职责-比如数据缓存和数据绑定-放在一个类中。

  • 每个模块暴露的细节越少越好。不要图一时爽快把模块的内部实现暴露出来。你可能当时节省了一点时间,但随着代码的演变,你可能会付出不知翻了多少倍的技术债。

  • 在定义模块之间的交互时,思考如何做到各个模块的独立和可测试。比如,一个设计良好的网络数据请求API可以让缓存数据到本地数据库模块的测试更加简单。而如果把这两个模块的逻辑混到一起,或者把网络请求的代码分散到代码的各处,都会使测试变得很难。

  • app的核心应该可以让app脱颖而出的那些东西,不要把时间花在重复造轮子和反复写相同代码的事情上。相反,你应该把自己的精力集中到如何使app独一无二上。重复的工作就交给Android Architecture Components以及推荐的库来处理吧。

  • 尽可能的持久化重要的,新生的数据,以便让你的app在离线状态下都可用。虽然你可能享受着高速的网络,但你的用户也许不是。

  • repository应该指派一个可以用作single source of truth的数据源。每当app需要获取一块数据的时候,它总是来自这个single source of truth。更多的信息参见上面的Single source of truth

附:暴露网络状态

在前面app架构推荐一节中,为了保持例子的简单,我们有意省略了网络错误以及加载状态。这一节我们将演示一种暴露网络状态的方法,使用一个Resource类来封装数据以及数据的状态。

下面是一个实现的例子:

<pre class="brush:js;toolbar:false prettyprint linenums prettyprinted" style="box-sizing: border-box; overflow: auto; font-family: Menlo, "Bitstream Vera Sans Mono", "DejaVu Sans Mono", Monaco, Consolas, monospace; font-size: 14px; display: block; padding: 1.5em; margin: 0px 0px 20px; line-height: 1.42857; word-break: break-all; word-wrap: break-word; color: rgb(51, 51, 51); background: rgb(47, 54, 64); border: 0px !important; border-radius: 0px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">

  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. public static <T> Resource<T> success(@NonNull T data) {

  12. return new Resource<>(SUCCESS, data, null);

  13. }

  14. public static <T> Resource<T> error(String msg, @Nullable T data) {

  15. return new Resource<>(ERROR, data, msg);

  16. }

  17. public static <T> Resource<T> loading(@Nullable T data) {

  18. return new Resource<>(LOADING, data, null);

  19. }

  20. }

</pre>

因为从网络加载数据而显示从磁盘读出的数据是一种常见的用例,我们将创建一个可以在多个地方重用的helper类:NetworkBoundResource。

下面是NetworkBoundResource的决策树:

network-bound-resource.png

首先从监听database获取resource开始。当第一次从数据库加载的时候,NetworkBoundResource检查得到的结果是否可以分发或者需要从网络获取。注意这两者可能同时发生,因为当从网络更新缓存数据的时候往往还需要显示数据。

如果网络调用成功完成,将响应结果保存到数据库中,然后重新初始化数据流。如果请求失败,我们直接发出一个错误信号。

下面是NetworkBoundResource类为其子类提供的公共API:

<pre class="brush:js;toolbar:false prettyprint linenums prettyprinted" style="box-sizing: border-box; overflow: auto; font-family: Menlo, "Bitstream Vera Sans Mono", "DejaVu Sans Mono", Monaco, Consolas, monospace; font-size: 14px; display: block; padding: 1.5em; margin: 0px 0px 20px; line-height: 1.42857; word-break: break-all; word-wrap: break-word; color: rgb(51, 51, 51); background: rgb(47, 54, 64); border: 0px !important; border-radius: 0px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">

  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. // Called with the data in the database to decide whether it should be

  8. // fetched from the network.

  9. @MainThread

  10. protected abstract boolean shouldFetch(@Nullable ResultType data);

  11. // Called to get the cached data from the database

  12. @NonNull @MainThread

  13. protected abstract LiveData<ResultType> loadFromDb();

  14. // Called to create the API call.

  15. @NonNull @MainThread

  16. protected abstract LiveData<ApiResponse<RequestType>> createCall();

  17. // Called when the fetch fails. The child class may want to reset components

  18. // like rate limiter.

  19. @MainThread

  20. protected void onFetchFailed() {

  21. }

  22. // returns a LiveData that represents the resource

  23. public final LiveData<Resource<ResultType>> getAsLiveData() {

  24. return result;

  25. }

  26. }

</pre>

注意上面的类定义了两个类型的参数(ResultType,RequestType),因为API返回的数据类型可能和本地使用的数据类型不一致。

同时注意上面的代码使用了 ApiResponse 作为网络请求, ApiResponse 是对Retrofit2.Call 类的简单封装,用于将其响应转换为LiveData。

下面是NetworkBoundResource类其余的实现:

<pre class="brush:js;toolbar:false prettyprint linenums prettyprinted" style="box-sizing: border-box; overflow: auto; font-family: Menlo, "Bitstream Vera Sans Mono", "DejaVu Sans Mono", Monaco, Consolas, monospace; font-size: 14px; display: block; padding: 1.5em; margin: 0px 0px 20px; line-height: 1.42857; word-break: break-all; word-wrap: break-word; color: rgb(51, 51, 51); background: rgb(47, 54, 64); border: 0px !important; border-radius: 0px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">

  1. public abstract class NetworkBoundResource<ResultType, RequestType> {

  2. private final MediatorLiveData<Resource<ResultType>> result = new MediatorLiveData<>();

  3. @MainThread

  4. NetworkBoundResource() {

  5. result.setValue(Resource.loading(null));

  6. LiveData<ResultType> dbSource = loadFromDb();

  7. result.addSource(dbSource, data -> {

  8. result.removeSource(dbSource);

  9. if (shouldFetch(data)) {

  10. fetchFromNetwork(dbSource);

  11. } else {

  12. result.addSource(dbSource,

  13. newData -> result.setValue(Resource.success(newData)));

  14. }

  15. });

  16. }

  17. private void fetchFromNetwork(final LiveData<ResultType> dbSource) {

  18. LiveData<ApiResponse<RequestType>> apiResponse = createCall();

  19. // we re-attach dbSource as a new source,

  20. // it will dispatch its latest value quickly

  21. result.addSource(dbSource,

  22. newData -> result.setValue(Resource.loading(newData)));

  23. result.addSource(apiResponse, response -> {

  24. result.removeSource(apiResponse);

  25. result.removeSource(dbSource);

  26. //noinspection ConstantConditions

  27. if (response.isSuccessful()) {

  28. saveResultAndReInit(response);

  29. } else {

  30. onFetchFailed();

  31. result.addSource(dbSource,

  32. newData -> result.setValue(

  33. Resource.error(response.errorMessage, newData)));

  34. }

  35. });

  36. }

  37. @MainThread

  38. private void saveResultAndReInit(ApiResponse<RequestType> response) {

  39. new AsyncTask<Void, Void, Void>() {

  40. @Override

  41. protected Void doInBackground(Void... voids) {

  42. saveCallResult(response.body);

  43. return null;

  44. }

  45. @Override

  46. protected void onPostExecute(Void aVoid) {

  47. // we specially request a new live data,

  48. // otherwise we will get immediately last cached value,

  49. // which may not be updated with latest results received from network.

  50. result.addSource(loadFromDb(),

  51. newData -> result.setValue(Resource.success(newData)));

  52. }

  53. }.execute();

  54. }

  55. }

</pre>

现在我们就可以在repository中使用NetworkBoundResource来写实现用户的磁盘和网络操作了。

<pre class="brush:js;toolbar:false prettyprint linenums prettyprinted" style="box-sizing: border-box; overflow: auto; font-family: Menlo, "Bitstream Vera Sans Mono", "DejaVu Sans Mono", Monaco, Consolas, monospace; font-size: 14px; display: block; padding: 1.5em; margin: 0px 0px 20px; line-height: 1.42857; word-break: break-all; word-wrap: break-word; color: rgb(51, 51, 51); background: rgb(47, 54, 64); border: 0px !important; border-radius: 0px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">

  1. class UserRepository {

  2. Webservice webservice;

  3. UserDao userDao;

  4. public LiveData<Resource<User>> loadUser(final String userId) {

  5. return new NetworkBoundResource<User,User>() {

  6. @Override

  7. protected void saveCallResult(@NonNull User item) {

  8. userDao.insert(item);

  9. }

  10. @Override

  11. protected boolean shouldFetch(@Nullable User data) {

  12. return rateLimiter.canFetch(userId) && (data == null || !isFresh(data));

  13. }

  14. @NonNull @Override

  15. protected LiveData<User> loadFromDb() {

  16. return userDao.load(userId);

  17. }

  18. @NonNull @Override

  19. protected LiveData<ApiResponse<User>> createCall() {

  20. return webservice.getUser(userId);

  21. }

  22. }.getAsLiveData();

  23. }

  24. }

</pre>

原文地址:https://developer.android.com/topic/libraries/architecture/guide.html

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容