欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Android - Architecture (Resource & NetworkBoundResource)

程序员文章站 2022-07-15 13:10:15
...

最近在研究Architecutre与相应的Demo-GithubBrowserSample,同时也利用这些技术重写了DYTT。收获很多,但是迷惑的点也有很多。

相信看过GithubBrowserSampleArchitecture引导的朋友对ResourceNetworkBoundResource一定不会陌生,今天主题也主要围绕这些方面:

  1. Resource的作用是什么?为什么要有它、好处是什么?
  2. NetworkBoundResource的作用是什么?它是如何被设计的?
  3. NetworkdBoundResource如何测试它?

Resource

这里就不贴Resource实现的代码了,你可以在这里或者这里找到它的实现。

  1. In this section, we demonstrate a way to expose network status using a Resource class to encapsulate both the data and its state.
  2. A generic class that describes a data with a status

从上面官方对它的描述来看,Resource一个持有数据及其状态的通用类,而且它主要用于暴露网络状态,这也就不难理解理解为什么它包含LOADINGSUCCESS以及ERROR这三种状态了,它们分别对应网络加载LoadingSuccess以及Failure状态。

虽然是主要用于暴露网络状态,但是实际上Android应用中各种类型的数据加载 - 例如:数据库加载,都逃不开对这三种状态(LOADING/SUCCESS/ERROR)的处理,所以可以认为Resource包装的三种状态是通用的。

Resource的作用:将一个加载过程分拆为不同的状态(LOADING/SUCCESS/ERROR),并将数据与状态相绑定,也额外提供一些必要的信息。

Resource的好处:

  1. 分拆加载状态,逻辑更清晰。
  2. 对数据与状态进行封装,增强可扩展性。

NetworkBoundResource

  1. Because loading data from network while showing it from the disk is a common use case, we are going to create a helper class NetworkBoundResource that can be reused in multiple places.
  2. A generic class that can provide a resource backed by both the sqlite database and the network.

通过官方文档对其的描述来看,NetworkBoundResource可以提供由数据库和网络支持的资源,通常用于加载网络资源的同时还需要显示数据库数据的情况。

而且NetworkBoundResource是在遵循Single Source Of Truth(单一数据源)的原则下设计的。

Single source of truth
- It is common for different REST API endpoints to return the same data. For instance, if our backend had another endpoint that returns a list of friends, the same user object could come from two different API endpoints, maybe in different granularity. If the UserRepository were to return the response from the Webservice request as-is, our UIs could potentially show inconsistent data since the data might change on the server side between these requests. This is why in the UserRepository implementation, the web service callback just saves the data into the database. Then, changes to the database will trigger callbacks on active LiveData objects.
- In this model, the database serves as the single source of truth, and other parts of the app access it via the repository. Regardless of whether you use a disk cache, we recommend that your repository designate a data source as the single source of truth to the rest of your app.

一个类的行为应该由决策树来指导,当一个类的决策树确定时那么该类也就设计完成了,剩下只需要将逻辑填充完整即可:

Android - Architecture (Resource & NetworkBoundResource)

NetworkBoundResource的实现可以在这里或者这里找到。

NetworkBoundResource的流程是从观察数据库开始的。当数据第一次从数据库中加载时(通常是APP显示时,数据库感知到生命周期发生变化,进而触发数据回调),它会检查数据是否可以用于直接分发,或者是否应该从网络获取数据。如果网络获取数据成功,那么结果会被存入到数据库中;如果获取失败,那么将会直接分发失败的结果。当结果被存入到数据库中时,数据库会对改变的数据进行分发。

根据对以上决策树以及逻辑的分解,可以将NetworkBoundResource分解为以下关键方法:

// 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, implemented
    // in the base class.
    public final LiveData<Resource<ResultType>> getAsLiveData();
}

NetworkBoundResource的作用:显示数据库缓存的同时可以加载网络数据,并将其结果更新到数据库中,再由数据库分发到UI层。
NetworkBoundResource的设计意图:在单一数据源原则下设计的数据加载帮助类(数据库/网络)。

由于NetworkBoundResource主要是将数据与状态封装成Resource进行分发的,根据NetworkBoundResource的逻辑执行状态,也可以分为以下执行流:

  1. Resource.Loading() - Common => (Not Fetch Data) Resource.Success() - DbSource
  2. Resource.Loading() - Common => (Need Fetch Data) Resource.Loading() - DbSource
    1. => (Fetched Data Failure) Resource.Error() - DbSource
    2. => (Fetched Data Success) Resrouce.Success - NewDbSource

此外NetworkBoundResource中在使用MediatorLiveData时,内部OnChange()通常都是先Remove(source)Add(Source),这样做的含义是:

we re-attach dbSource as a new source, it will dispatch its latest value quickly

1. 保证数据是最新的(LiveData.setValue()可以被调用多次)

protected void setValue(T value) {
      assertMainThread("setValue");
      mVersion++;
      mData = value;
      dispatchingValue(null);
}

2. 保证一定可以能获取到数据

private void dispatchingValue(@Nullable ObserverWrapper initiator) {
    if (mDispatchingValue) {
        mDispatchInvalidated = true;
        return;
    }
    mDispatchingValue = true;
    do {
        mDispatchInvalidated = false;
        if (initiator != null) {
            considerNotify(initiator);
            initiator = null;
        } else {
            // Important
            // mObservers是SafeIterableMap类型 - LinkedList, which pretends to be a map and supports modifications during iterations.
            // 这样就可以保证onChange中addSource添加的回调一定会被调用
            for (Iterator<Map.Entry<Observer<T>, ObserverWrapper>> iterator =
                    mObservers.iteratorWithAdditions(); iterator.hasNext(); ) {
                considerNotify(iterator.next().getValue());
                if (mDispatchInvalidated) {
                    break;
                }
            }
        }
    } while (mDispatchInvalidated);
    mDispatchingValue = false;
}

你还可以参考这里:

NetworkdBoundResource Test Case

相信上面说了这么多,NetworkBoundResource的测试用例一定可以被划分出来了:

  1. db success without network (数据库加载成功且没有网络加载)
  2. db success with network success (数据库加载成功且网络加载成功)
  3. db success with network failure (数据库加载成功且网络加载失败)
  4. network success (纯网络加载成功 - 沒弄懂为什么要这么做)
  5. network failure (纯网络加载失败 - 沒弄懂为什么要这么做)

按照TDD的路子,应该是先细分需求,再设计类的功能,再根据功能写测试用例,再根据Red - Green - Refactor - Red的步骤不断实现类。 但是由于水平有限,就只能根据现有类倒推测试用例了。

下面的测试用例也很简单,先跟着步骤1/步骤2/步骤3走就可以了,缺那里补哪里。

 @Test
public void dbSuccessWithNetworkSuccess() {
    final Foo dbValue = new Foo(1);
    final Foo dbValue2 = new Foo(2);
    final AtomicReference<Foo> saved = new AtomicReference<>();

    // 是否抓取数据
    shouldFetch = new Function<Foo, Boolean>() {
        @Override
        public Boolean apply(Foo foo) {
            return foo == dbValue;
        }
    };

    // 网络数据
    final MutableLiveData<ApiResponse<Foo>> apiResponseLiveData = new MutableLiveData();
    createCall = new Function<Void, LiveData<ApiResponse<Foo>>>() {
        @Override
        public LiveData<ApiResponse<Foo>> apply(Void aVoid) {
            return apiResponseLiveData;
        }
    };

    // 存储网络数据
    saveCallResult = new Function<Foo, Void>() {
        @Override
        public Void apply(Foo foo) {
            saved.set(foo);
            dbData.setValue(dbValue2);
            return null;
        }
    };

    // 步骤1:初始化
    Observer<Resource<Foo>> observer = Mockito.mock(Observer.class);
    networkBoundResource.getAsLiveData().observeForever(observer);
    drain();
    verify(observer).onChanged(Resource.<Foo>loading(null));

    // 步骤2:验证数据库观察结果
    dbData.setValue(dbValue);
    drain();
    verify(observer).onChanged(Resource.<Foo>loading(dbValue));

    // 步骤3:验证网络请求成功
    final Foo networkResult = new Foo(1);
    apiResponseLiveData.setValue(new ApiResponse<Foo>(Response.success(networkResult)));
    drain();
    assertThat(saved.get(), is(networkResult));
    verify(observer).onChanged(Resource.<Foo>success(dbValue2));

    // 验证是否还有未验证的行为
    verifyNoMoreInteractions(observer);
}