Guide to App Architecture 全文翻译

原文地址:https://developer.android.com/topic/libraries/architecture/guide.html#the_final_architecture 建议看原文.

本指南适用于具备一定APP编程继承的开发者,并且现在想要知道如何通过一个最佳的实践和使用推荐的架构去建立一个健全、具备产品质量(Production Quality)的软件。

APP开发者常见的问题:

不同于传统的桌面系统(desktop counterparts)在大部分情况下,在应用启动的快捷方式中拥有一个单一的入口并且当前应用会运行在一个独立的进程(monolithic process)中,但是安卓的APP有着一个更复杂的结构。一个典型的安卓APP由多个app组件构成,其中包括activitis、fragments、services、content providers和broadcast receivers。

大部分APP组件在app manifest中有着声明,以决定如何将应用程序集成到其设备的整体用户体验中。然而,在先前提过,一个桌面类型APP通常运行在一个独立的进程中,一个正确编写的安卓app需要变得更灵活因为用户会在不同的app中穿梭,不断切换流程和业务(constantly switching flows and tasks)。

比如说,思考一下当你往你的网络社交软件中分享一张照片时会发生什么。该程序触发相机意图(intent),Android 系统将开启camera app 去处理这种请求。在这个时候,用户离开网络社交软件但他们的体验是无缝的。照相APP同样也可能触发其他意图,比如说开启文件选择等,也会开启其他app。此外,用户在使用此程序的任何时候都可能会被一个电话打断,打完电话后用户可以返回并分享照片。

在Andorid当中,这个app跳跃(app-hopping)行为是很常见的,所以你的app必须正确的处理这些流动过程。时刻记住移动设备是资源有限的,所以在任何时候,操作系统都有可能杀死某些程序以获取新的空间,并将这些空间提供给新的程序。

总的来说,你的app组件可以独立启动或者无序启动,并且可以在任何时候被用户或者系统摧毁。因为APP的组件是短暂的并且他们的生命周期(当他被创建或销毁)不在你的控制之下,你不应该在你的app组件中存储任何app数据或者状态,并且你的app组件不应该存在互相依赖的关系。

常见的架构原则(Common architectural principles)

如果不能使用app组件去存储app数据和状态,那么app如何结构化?

你需要关注的最重要的事情就是SoC( separation of concerns),一个常见的错误就是把你所有的代码都写在Activity或者Fragment当中,任何不处理UI或操作系统交互的代码都不应该在这些类中(有点拗口,实际上是UI处理或系统交互的代码才应该放进这两个类中)。让他们保持尽可能的精简将让你更好的避免处理过多的生命周期相关的事情。不要忘记一件事:你不会拥有这些类,他们只是黏贴在系统OS和你的应用程序之间的契约类。Android系统可能会在任何时候销毁他们,比如说用户的交互或者其他的因素:低电量。最好尽量减少对他们的依赖关系以提供一个更可靠的用户体验。

第二个重要的原则就是从model中驱动你的UI,最好是一个持久化的(Persistent)model。持久化(Persistent)是最理想的选择基于两个原因:在系统销毁你的APP去释放某些资源的时候,你的用户不希望丢失数据。在网络连接不通畅或者无网络连接的时候你的app将继续工作。Models是APP当中负责处理数据的组件。他们独立于视图和APP组件,因此他们和组件的生命周期问题相隔离。将你的UI代码保持简单并且没有应用逻辑能让他更好的被管理。将你的app建立在拥有明确管理数据职责的model类的基础之上,将提高可测试性和app的连贯性。

推荐的APP架构

在这个模块,我们演示了如何去使用 Architecture Components去构建一个app。

NOTE:不可能去写出一个方法让他适用于任何场景。话虽如此,这个推荐的架构对于大部分用例来说是一个良好的开始。如果你已经拥有一个好的方法去编写你的Android APP,那么你不需要去改变。

想象一下我们构建一个展示用户档案的UI。用户档案从我们私有后台提供的REST API获取。

创建user接口

UI由一个fragment组成(UserProfileFragment.java),他的layout文件是user_profile_layout.xml。

为了驱动UI,我们的data model需要去持有两个数据元素:

  • User ID:user用户的识别码(译者注:类似主键),最好的方式就是通过fragment arguments将这个信息传入到fragment当中。如果Android系统销毁了你的进程,这个信息会被保留所以下次程序启动时此ID可用。
  • The User object:持有user data的POJO。

创建一个继承ViewModel 的UserProfileViewModel 去持有这个信息。
ViewModel给特定的UI组件提供数据,比如说fragment或者activity,并且处理 业务之间的数据处理通信:调用其他组件去获取数据或者转发用户的修改信息。ViewModel并不直到View的信息,也不会受到配置更改的影响,比如说activity因为横竖屏切换而重建。

现在我们拥有三个文件:

  • 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 LifecycleFragment {
    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);
    }
} ```

>Note:上面的例子继承了 [LifecycleFragment
](https://developer.android.com/reference/android/arch/lifecycle/LifecycleFragment.html)而不是Fragment,在Architecture Components中的Lifecycles API变得稳定后,Android Support 中的Fragment类将继承[LifecycleOwner
](https://developer.android.com/reference/android/arch/lifecycle/LifecycleOwner.html).

现在我们有了这三个代码模块,那么我们如何将他们联系起来?归根究底,当ViewModel的user参数被设置,我们需要一个方法去通知UI。所以这里提出了LiveData 类

 **[LiveData](https://developer.android.com/topic/libraries/architecture/livedata.html)**是一个可观察(Observable)的数据持有者(data holder)。他能让你的app组件在和他之间不创建明确且死板的依赖关系的情况下观察LiveData对象的变化。LiveData也遵循app组件的声明周期状态,做正确的事情去阻止对象泄漏(object leaking),所以你的app不会消耗过多的内存。

>Note:如果你已经在使用第三方库例如Rxjava或者Agera,你也可以使用它们来替代LiveData.但是当你使用它们或者其他类似的方法的时候,却把你正确处理了生命周期,例如在相关的LifecycleOwner 停止的时候数据流会暂停、LifecycleOwner销毁时数据流也销毁。你也可以添加android.arch.lifecycle:reactivestreams ,将LiveData和其他的库一起使用(Rxjava2)。

 现在我们替换UserProfileViewModel中的User参数为LiveData<User>,因此当数据更新时fragment会被通知。LiveData最好的一个地方在于他能察觉生命周期,在他们不被需要的时候自动清除引用。

public class UserProfileViewModel extends ViewModel {
...
private User user;
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
    });
}```
每当user 数据更新后,onChanged回调将会被调用,UI会被刷新。

如果你熟悉使用其他可观察回调的库,你可能会意识到我们没有重写fragment的onStop()方法去停止观察数据。当我们使用LiveData的时候着不是一个必须的事情因为他具备生命周期感知,这也意味着除非fragment是一个活跃的状态,不然他不会调用回调方法(在onStart能接受,在onStop不能)。LiveData也能自动移除观察者,当fragment调用onDestroy.

我们也没有做任何特殊的事情去处理配置改变(例如屏幕旋转)。在配置改变后ViewModel会自动恢复,所以当新的fragment到来,它将接受到相同的ViewModel对象并且使用当前数据的回调会立即被调用。这就是为什么ViewModel不能直接引用View的原因,他们能独立于View的生命周期生存。See [The lifecycle of a ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel.html#the_lifecycle_of_a_viewmodel).

####取出数据
现在我们将ViewModel连接上fragment,那么怎么让ViewModel去获取user数据?在这个例子里,我们假设我们的后台提供一个REST API。我们将使用Retrofit库去访问后台。

这里展示了和后台通信的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<User> getUser(@Path("user") String userId);
}```
ViewModel的一个简单的实现就是直接调用Webservice去获取数据并将其分配给user对象。尽管这样写可以实现,你的app在后期会难以维护。它给予ViewModel过多的职责以至于违反了SOC原则。另外,ViewModel的活动范围和Activity与Fragment的声明周期紧密相连,所以在生命周期结束的时候丢失所有数据会引起不好的用户体验。我们的ViewModel应该将这项工作委派给一个新的Repository模块(译者注:可以参考我的Repository Pattern啊!!!)。

Repository模块(Repository Modules)负责处理数据。他们给app其余部分提供一个干净的api。当数据更新后他们知道从哪里去获取数据并调用确切的API。你可以认为他们是不同数据源之间的中介者。(persistent model,web service,cache等)

下面的UserRepository调用WebService去获取user的数据。

public class UserRepository {
    private Webservice webservice;
    // ...
    public LiveData<User> getUser(int userId) {
        // This is not an optimal implementation, we'll fix it below
        final MutableLiveData<User> data = new MutableLiveData<>();
        webservice.getUser(userId).enqueue(new Callback<User>() {
            @Override
            public void onResponse(Call<User> call, Response<User> response) {
                // error case is left out for brevity
                data.setValue(response.body());
            }
        });
        return data;
    }
}

尽管repository模块看起来不是必要的,但他表明了一个重要的意图:对app其余部分抽象了数据来源。现在我们的ViewModel并不知道数据是从Webservice获取的,这也意味着我们可以根据需求替换为其他的实现。

Note:为了简单起见我们排除了网络错误。对于暴露错误或者加载状态的替换可以看:see Addendum: exposing network status

管理组件之间的依赖关系

他的对象,但是如果仅仅是这么做了,那么我们也需要知道Webservice类的依赖关系来构造他。这会让代码变得复杂且重复。(每个需要构造Webservice的类都需要知道怎么通过Wevservice类的依赖关系来构造出他的对象)。另外,UserRepository可能不是唯一一个需要Webservice的类。如果每个类都重复构造一个新的Webservice,这将浪费大量资源。

这里由两个方案去解决这个问题:

  • 依赖注入(Dependency Injection):依赖注入允许类定义他们的依赖关系而不构造他们。在运行时,另一个类负责提供这些依赖。我们推荐使用dagger2去实现依赖注入。Dagger2通过游走于依赖树自动构造对象并且在编译期提供依赖关系的保证。

  • 服务定位器(Service Locator):定位器提供一个注册表,其中类可以直接获取他们的依赖关系,而不是直接构造他们。相对于依赖注入来说这个比较好实现。如果你不熟悉DI,那就用这个。

它们提供明确的模式来管理依赖关系,而不会让代码重复或增加复杂性。并且他们都允许交替实现来进行测试。

在这个例子中我们使用dagger2来管理依赖关系。

联立ViewModle和repository

我们修改UserProfileViewModel去使用repository.

public class UserProfileViewModel extends ViewModel {
    private LiveData<User> 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<User> getUser() {
        return this.user;
    }
}```


###缓存数据
上面对于repository的实现很好地抽象了网络服务的调用,但是他只是依赖于一个数据源,所以不算很实用。

UserRepository的问题在于他只是获取到了数据但是没有对数据进行存储。如果用户离开了UserProfileFragment后返回,app会重新获取数据。两个原因体现他们的缺点:
1.浪费了宝贵的网络带宽
2.迫使用户等待新的查询完成
为了解决这个问题,我们将给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<User> getUser(String userId) {
LiveData<User> cached = userCache.get(userId);
if (cached != null) {
return cached;
}
final MutableLiveData<User> 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<User>() {
@Override
public void onResponse(Call<User> call, Response<User> response) {
data.setValue(response.body());
}
});
return data;
}
}```

持久化数据

在我们目前的实现中,如果用户旋转屏幕或者离开后再返回app,UI将立即可见,因为Repository从内存缓存中获取数据。但是如果用户离开了app,并且在几个小时Android系统杀死进程后才回来,那么会发生什么?

在目前的实现中,我们需要重新去网络中获取数据。这不仅仅是一个不好的用户体验,也是一个资源上的浪费因为需要重新获取相同的数据。你可以通过缓存Web请求来解决这个问题,但这也产生了一个新的问题。如果另一种类型的请求也出现了同样的user数据获取会发生什么情况(比如说获取朋友列表)?那么你的app将可能显示不一致的数据,这是最令人困惑的用户体验。例如:同样是user数据可能显示出来的不一样,因为friend列表的请求和
user的请求可能在不同的时间段发生。你的app需要去合并他们以防止展示不一致的数据.(译者注:主要是去重和局部刷新合并,如果不用db就不好解决,单纯用http缓存是不够的,这部分在有心课堂的一起做个即时通信APP有所提及并提出了问题的产生以及实际解决方案)

Room是一个对象映射的库,使用最小的模板代码去实现本地数据持久化存储。在编译期,他根据模式验证每个查询,所以错误的SQL查询会导致编译期的错误,而不是运行时故障。Room抽象了使用原始SQL标和查询的底层实现细节。他也可以观察数据库数据的改变
(包括集合和联合查询),通过LiveData暴露出这些更改。此外,他明确的定义了线程约束解决了常见的问题,比如所通过主线程去访问
存储。

Note:如果你对其他的持久化方案很熟悉,比如说SQLite ORM或者其他的数据库比如Realm,你不需要去将他们替换成Room,除非Room的特性更符合你的用例。

为了使用Room,我们需要去定义本地的shema。首先使用@Entity注释User类,将其标记为数据库中的一个表。

@Entity
class User {
  @PrimaryKey
  private int id;
  private String name;
  private String lastName;
  // getters and setters for fields
}```

然后,继承[RoomDatabase
](https://developer.android.com/reference/android/arch/persistence/room/RoomDatabase.html)建立一个数据库类:

@Database(entities = {User.class}, version = 1)
public abstract class MyDatabase extends RoomDatabase {
}```
注意MyDatabase是一个抽象类。Room会自动给他提供一个实现类。

现在我们需要一个方法去给数据库插入user数据。我们创建一个 data access object (DAO).

@Dao
public interface UserDao {
    @Insert(onConflict = REPLACE)
    void save(User user);
    @Query("SELECT * FROM user WHERE id = :userId")
    LiveData<User> load(String userId);
}```
然后,从我们的数据库类中引用DAO。

@Database(entities = {User.class}, version = 1)
public abstract class MyDatabase extends RoomDatabase {
public abstract UserDao userDao();
}```
注意load方法返回一个LiveData<User>.Room知道数据库何时被更改,并且当数据改变时他会自动通知所有活跃的观察者。因为它使用的是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<User> 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或者UserProfilrFragment。这就是抽象带来的灵活性。同样这也便于测试因为在测试UserProfileViewModel的时候你可以提供一个假的UserRepository.

现在我们写完代码了。当用户几天后返回到相同的UI界面,他们会立即看到user的信息因为我们将其持久化存储。同时如果数据过时我们的repository将在后台更新数据。当然这取决于你的用例,如果数据太老旧你可能不会让他展示出来。

在某些用例中,比如说下拉刷新,如果当前正在进行网络操作那么UI界面展示user数据是有必要的。将UI与实际数据分开是一个很好的做法,因为数据可能由于某种原因被更新。(比如说如果我们获取friends列表数据,user数据可能会被再次获取引起LiveData<User>的更新)。从UI的角度来看,飞行模式的需要是既定的事实,这算是另一个数据点 ,和其他的数据片段一样(就像User对象一样).

这种用例有两个常见的解决方案:
- 更改getUser方法去返回一个包含网络操作状态的LiveData。 [Addendum: exposing network status](https://developer.android.com/topic/libraries/architecture/guide.html#addendum)模块展现了一个实现例子。

- 在Repository类中提供另一个public方法,此方法返回User的刷新状态。如果仅仅是在用户界面中显示网络状态,那么这种选项会更好。

####单一来源
不同的REST API端点返回相同的数据是很常见的。比如说,如果我们的后台有其他的端点返回friends列表,同样的user对象会从两个不同的API端点获取,可能具有不同的粒度。如果UserRepository按原样从Webservice请求后返回响应,我们的UI可能会展示不一致的数据,因为在这些请求之间的服务端数据可能被改变。这也是为什么UserRepository的实现中,网络端服务的回调会存储数据到数据库当中。然后数据库当中的更改触发活跃的LiveData对象的回调。(译者注:有点类似于数据库的脏读)。

数据库服务作为唯一的数据来源,app其余部分通过Repository访问他。不管你是否使用磁盘缓存,我们都推荐你的Repository选择一个数据源作为你数据唯一的真实来源。

####测试
我们曾提出过分离的其中一个好处就是可测试性。让我们看看如何对每个代码模块进行测试:
- User Interface&Interactions:这是唯一的需要使用[Android UI Instrumentation test](https://developer.android.com/training/testing/unit-testing/instrumented-unit-tests.html)的时候。最好的方式去测试UI代码就是去创建一个 [Espresso](https://developer.android.com/training/testing/ui-testing/espresso-testing.html)。你可以创建fragment,给他提供一个模拟出来的ViewModel.因为fragment只和ViewModel之间有通信,模拟ViewModel足以对UI进行完善的测试。

- ViewModel:ViewModel可以用JUnit test 进行测试。你只需要模拟出UserRepository对他进行测试。

- UserRepository:你同样可以使用JUnit对他进行测试。你需要去模拟WebService和DAO.你可以测试他是否进行正确的Web服务调用,将结果存入数据库,如果数据已经被缓存并且是最新的数据,那么不会发出任何不必要的请求。WebService和UserDao都是接口,你可以模拟他们或为一个复杂的测试用例创建一个假的实现。

- UserDao:推荐使用instrumentation tests去测试DAO类。instrumentation test不需要任何UI并且运行速度快。对于每个测试,你可以在内存中创建一个数据库去确保测试不会产生任何副作用。(比如说更改硬盘中的数据库文件)

Room允许指定数据库进行实现所以你可以通过使用[SupportSQLiteOpenHelper
](https://developer.android.com/reference/android/arch/persistence/db/SupportSQLiteOpenHelper.html)的Junit实现来进行测试。这种途径通常来说不是很推荐因为设备中的Sqlite版本可能和你主机上的机器不一样。


- Webservice:将测试独立于外部世界是很重要的,所以你的WebService需要避免和后台进行网络访问。可以找到很多库来帮助我们进行操作。比如说[MockWebServer](https://github.com/square/okhttp/tree/master/mockwebserver) 是一个很好的库以帮助你创建一个假的本地服务来帮助你进行测试。

- Testing Artifacts:Architecture Components提供了一个Maven组件来控制它的后台线程。在android.arch.core:core-testing内部   这里有两个JUnit的规则:
1.InstantTaskExcutorRule:
这条规则可以用来强制Architecture Components在调用他的线程当中(calling thread)进行任何后台操作.

2.CountingTaskExecutorRule:这个可以在instrumentation test当中使用去等待Architecture Components的后台操作或将他和Espresson联立作为一个空闲资源。

####The final Architecture
下面的图展示了我们推荐的架构当中所有模块以及他们之间的依赖关系。

![](http://upload-images.jianshu.io/upload_images/3267534-509ff6d8317e24ed.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

###主导原则
编程是一个创造性的事情,编写Android app也不例外。有很多种不同的方法去解决一个问题,无论实在多个activity或者fragment之间传递数据,获取远程数据还是为了在离线模式中保留在本地,或者是任何其他非同寻常的app遇到的常见场景。

尽管下面的建议不是强制性的,但那些都是我们总结出来的经验,遵循这些建议将是你的代码变得更健全,可测试并且从长远来看具备可维护性。
1.你在你的manifest中定义的入口点(entry points)-activities,services,broadcast receivers等等-不是数据的来源,相反他们只能协调与入口点相关数据子集。根据用户与设备之间的互动以及运行时的整体运行状况,每个app组件都是相对来说比较命短的,你不会希望你的入口点变成数据源。

2.在app当中的各个模块之间创建明确的责任界限。比如说,将访问网络获取数据的代码传播到多个类或包中。同样的,不要填充不相关的责任:比如说将数据缓存和数据绑定放在同一个类中。

3.各个模块间尽可能少的暴露内部实现。不要贪图一时之快就从公开一个模块的内部细节。你可能会在短期内从中获取一些好处(比如时间),但是随着代码库的发展,你就会多次支付技术债务。(译者注:痛苦!!!!!!!!!!!!!)

4.当你定义各模块之间的交互时,考虑一下如何让两者之间的测试独立。比如说,拥有一个从网络当中获取数据的明确的API将会使得开发者更容易对数据库缓存模块进行测试。相反,你把两个模块的逻辑混合在一个地方,或者将网络访问的代码遍布在代码库的各个地方,这简直难以测试,啥垃圾玩意!

5.你的app的核心应该是能够让你的作品脱颖而出的东西。不要浪费你的时间重复造轮子或者重复写些一样的模板代码。把注意力集中在如何让你的app独一无二,让Android Architecture Component和其他推荐的库处理重复的样板代码。

6.尽可能多的持久化相关和新鲜的数据,从而让你的app在离线的状态下也是可用的。尽管你可能在享受着告诉且稳定的网络,但你的用户不是。

7.你的Repository需要选择一个唯一的真实数据来源。你的app无论在什么时候请求数据,他都应该从唯一的数据源获取数据。

### 暴露网络状态
在前面的一个模块中(推荐的APP架构)提到过,我们故意去忽略网络错误和loading状态去保证例子足够简单。在这个模块当中,我们演示了一个方法去使用Resource类封装数据和状态去暴露网络状态。

//a generic class that describes a data with a status
public class Resource<T> {
@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 <T> Resource<T> success(@NonNull T data) {
return new Resource<>(SUCCESS, data, null);
}
public static <T> Resource<T> error(String msg, @Nullable T data) {
return new Resource<>(ERROR, data, msg);
}
public static <T> Resource<T> loading(@Nullable T data) {
return new Resource<>(LOADING, data, null);
}
}```

从磁盘展示数据的同时通过网络载入这些数据是一种常见情况,我们将要创建一个辅助类NetWorkBoundResource,并让他能在不同的地方复用。下面是NetWorkBoundResource的决策树:


他通过观察数据库中的资源开始。当数据集第一次从数据库中加载,NetworkBoundResource检查结果是否足够好,以至于可以被分发或选择去网络中获取。请注意这两者可以同时发生,你可能希望在展示缓存数据时从网络端获取数据。

如果网络访问成功后,他将返回的结果存储到数据库中,并重新初始化流。如果网络访问错误,我们直接分发这个事故。

Note:在存储新的数据到硬盘中后,我们从数据库中重新初始化流,尽管我们通常不需要那么做,因为数据库将分发这种变化。另一方面,依赖数据库去分发这种变化将引起一个副作用那就是他可能会中断因为当数据没有改变的时候数据库会避免分发这种更改。我们也不希望去分发这个从网络中获取的结果因为这会破坏数据唯一的真实来源(single source of truth)也许数据库中有触发器会改变保存的值,我们也不希望在没有新数据的情况下直接分发Success,因为他会向客户端发送错误的信息。

下面是NetworkBoundResource类给他的子类提供的公共API:

// ResultType: Type for the Resource data
// RequestType: Type for the API response
public abstract class NetworkBoundResource<ResultType, RequestType> {
    // 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<ResultType> loadFromDb();
    // Called to create the API call.
    @NonNull @MainThread
    protected abstract LiveData<ApiResponse<RequestType>> 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
    public final LiveData<Resource<ResultType>> getAsLiveData() {
        return result;
    }
}```
注意上面的类定义了两个类型的参数(ResultType,RequestType),因为从API返回的数据类型和本地使用的数据类型不匹配。

同样也要注意上面的代码为网络请求使用了ApiResponse,APIResponse是对Retrofit2.Call类的一个简单封装,将其响应的类型转换为LiveData。

下面是NetworkBoundResource类的其他实现:

public abstract class NetworkBoundResource<ResultType, RequestType> {
private final MediatorLiveData<Resource<ResultType>> result = new MediatorLiveData<>();
@MainThread
NetworkBoundResource() {
result.setValue(Resource.loading(null));
LiveData<ResultType> 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<ResultType> dbSource) {
LiveData<ApiResponse<RequestType>> 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<RequestType> response) {
new AsyncTask<Void, Void, Void>() {
@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();
}
}```

现在我们在Repository中使用NetworkBoundResource去写入磁盘和访问网络获取User数据。

class UserRepository {
    Webservice webservice;
    UserDao userDao;
    public LiveData<Resource<User>> loadUser(final String userId) {
        return new NetworkBoundResource<User,User>() {
            @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<User> loadFromDb() {
                return userDao.load(userId);
            }
            @NonNull @Override
            protected LiveData<ApiResponse<User>> createCall() {
                return webservice.getUser(userId);
            }
        }.getAsLiveData();
    }
}```
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容