Android Jetpack 之分页:paging

找官方demo-PagingSample,

看一下官方的paging动态图,数据源里面插入一条数据的


image

其核心就三部分:DataSource(数据源)、PagedList(分页组件)、PagedListAdapter(适配器)
当数据源发生改变,自动通知Adapter改变,从而完成界面上的展示变化。使用观察者模式来完成,数据源就是被观察者对象,而Adapter则是观察者。

paging库源码的两部分


paging库

common是其抽象出来的代码,而runtime则是其实现的代码

看其抽象的类图
paging_class_diagram.jpg

可以看到DataSource有众多的实现,但是其划分也就两种,单点源和连续源。
单点源(PositionalDataSource):是指一次获取出来所有的数据,当数据源里面数据有增删改时,观察者自动更新。
连续源(ContiguousDataSource):连续源就是我们的分页加载,当下滑到一定的位置时,就自动获取新的一页数据。

官方demo,是单点源方式。相对要简单一些,添加一条数据,通过viewModel将数据保存到数据库,viewModel已改变,其观察者响应变化的

       //添加数据后的新数据 it 
        viewModel.allCheeses.observe(this, Observer(){
            adapter.submitList(it)
        })

上面代码是数据adapter改变,其内部通过子线程做数据对比,对比完成后,切换到主线程更新界面,对比完成后的,AsyncPagedListDiffer.latchPagedList方法完成界面的刷新,关键代码并见下

        // dispatch update callback after updating mPagedList/mSnapshot
        PagedStorageDiffHelper.dispatchDiff(mUpdateCallback,
                previousSnapshot.mStorage, newList.mStorage, diffResult);

dispatchDiff方法完成数据的回调,具体是插入,删除,修改,在这些判断 ,并完成回调。

   /**
     * TODO: improve diffing logic
     *
     * This function currently does a naive diff, assuming null does not become an item, and vice
     * versa (so it won't dispatch onChange events for these). It's similar to passing a list with
     * leading/trailing nulls in the beginning / end to DiffUtil, but dispatches the remove/insert
     * for changed nulls at the beginning / end of the list.
     *
     * Note: if lists mutate between diffing the snapshot and dispatching the diff here, then we
     * handle this by passing the snapshot to the callback, and dispatching those changes
     * immediately after dispatching this diff.
     */
    static  void dispatchDiff(ListUpdateCallback callback,
            final PagedStorage oldList,
            final PagedStorage newList,
            final DiffUtil.DiffResult diffResult) {

        final int trailingOld = oldList.computeTrailingNulls();
        final int trailingNew = newList.computeTrailingNulls();
        final int leadingOld = oldList.computeLeadingNulls();
        final int leadingNew = newList.computeLeadingNulls();

        if (trailingOld == 0
                && trailingNew == 0
                && leadingOld == 0
                && leadingNew == 0) {
            // Simple case, dispatch & return
            diffResult.dispatchUpdatesTo(callback);
            return;
        }

        // First, remove or insert trailing nulls
        if (trailingOld > trailingNew) {
            int count = trailingOld - trailingNew;
            callback.onRemoved(oldList.size() - count, count);
        } else if (trailingOld < trailingNew) {
            callback.onInserted(oldList.size(), trailingNew - trailingOld);
        }

        // Second, remove or insert leading nulls
        if (leadingOld > leadingNew) {
            callback.onRemoved(0, leadingOld - leadingNew);
        } else if (leadingOld < leadingNew) {
            callback.onInserted(0, leadingNew - leadingOld);
        }

        // apply the diff, with an offset if needed
        if (leadingNew != 0) {
            diffResult.dispatchUpdatesTo(new OffsettingListUpdateCallback(leadingNew, callback));
        } else {
            diffResult.dispatchUpdatesTo(callback);
        }
    }

连续源

在实际场景中,我们通常不会使用单点源,而是使用支持分页网络加载的连续源方式。而paging的连续源分页用户从体验上来说,几乎感觉不到有分页的,因为它在移动过程中并不是到底部才开始加载数据,而是在移动快到底部时就加载,这样给用户的感觉是将所有数据都加载出来了,提升了用户体验。
而使用连续源,DataSource根据需要使用PageKeyedDataSource或ItemKeyedDataSource。
其区别在于, 个人理解
PageKeyedDataSource:按页加载 1,2 ,3
ItemKeyedDataSource: 按索引加载,忽略前多少个
ViewModel数据更新后,adapter同样是使用submitList()方法,来刷新界面。 与单点源不同的是,内部的PagedList使用不同,单点源使用TiledPagedList,而连续源使用的ContiguousPgedList。
pagedList控制分布加载的关键点。
这里提几个问题:

  • Q1:初始化时,第一次请求怎么调用的?
  • Q2:快滑动到底部时是怎么自动加载数据的?

Q1:初始化时,第一次请求怎么调用的?

ViewModel 内部的列表一般会使用LiveData类型,而对于可变的列表,需要使用MutableLiveData。
其postValue()方法就是对DataSource源的设置。postValue()是一个异步方法,它还有个同步的方法setValue()。代码见下:

   protected void postValue(T value) {
        boolean postTask;
        synchronized (mDataLock) {
            postTask = mPendingData == NOT_SET;
            mPendingData = value;
        }
        if (!postTask) {
            return;
        }
        ArchTaskExecutor.getInstance().postToMainThread(mPostValueRunnable);
    }

可以看到它post了一个mPostValueRunnable ,在里面完成数据mData的设置。

    private final Runnable mPostValueRunnable = new Runnable() {
        @Override
        public void run() {
            Object newValue;
            synchronized (mDataLock) {
                newValue = mPendingData;
                mPendingData = NOT_SET;
            }
            //noinspection unchecked
            setValue((T) newValue);
        }
    };
    @MainThread
    protected void setValue(T value) {
        assertMainThread("setValue");
        mVersion++;
        mData = value;
        dispatchingValue(null);
    }

在paging里面提供了一个LivePagedListBuilder构建类,它作用是获得分布加载更多的LiveData实例。其内部使用可计算的LiveData(ComputableLiveData),它作为被观察者对象,其观察者响应结果mAdapter.submitList(it),从而完界面的刷新。
ComputableLiveData内部的compute()方法实现,
mList:它数据通过PagedList的建造者模式实例化出来。
mDataSource:工厂模式创建出真正的DataSource对象。
除了这两个重要的成员外,还有两个Runnable对象:mRefreshRunnable;分别负责刷新的回调,包括了下拉刷新,加载更多。只是下拉刷新的情况,它使用mInvalidationRunnable,其内部再调用mRefreshRunnable而已。

final Runnable mRefreshRunnable = new Runnable() {
        @WorkerThread
        @Override
        public void run() {
            boolean computed;
            do {
                computed = false;
                // compute can happen only in 1 thread but no reason to lock others.
                if (mComputing.compareAndSet(false, true)) {
                    // as long as it is invalid, keep computing.
                    try {
                        T value = null;
                        while (mInvalid.compareAndSet(true, false)) {
                            computed = true;
                            value = compute();
                        }
                        if (computed) {
                            mLiveData.postValue(value);
                        }
                    } finally {
                        // release compute lock
                        mComputing.set(false);
                    }
                }
            } while (computed && mInvalid.get());
        }
    };

    // invalidation check always happens on the main thread
    @VisibleForTesting
    final Runnable mInvalidationRunnable = new Runnable() {
        @MainThread
        @Override
        public void run() {
            boolean isActive = mLiveData.hasActiveObservers();
            if (mInvalid.compareAndSet(false, true)) {
                if (isActive) {
                    mExecutor.execute(mRefreshRunnable);
                }
            }
        }
    };

我们通常的下拉刷新,通过ComputableLiveData.invalidate()可重新获取数据,因为其mInvalid成员变量,当它设置为true,会调用compute(),这个方法是虚方法,看其具体实现,就是在LivePagedListBuilder构建的时候重写的,其后的mRefreshRunnable动作才会postValue()将新的值发送出去,观察者就立即响应。

    public void invalidate() {
        ArchTaskExecutor.getInstance().executeOnMainThread(mInvalidationRunnable);
    }

这就是下拉刷新的过程,ViewModel的执行过程,当然,界面上的刷新改变则是观察者响应的后续执行过程。

Q2:快滑动到底部时是怎么自动加载数据的?

自动执行了加载更多,关键点在哪?
答案就是:AsyncPagedListDiffer.getItem(),getItem()内部有一个关键的方法,见下

    public T getItem(int index) {
        if (mPagedList == null) {
            if (mSnapshot == null) {
                throw new IndexOutOfBoundsException(
                        "Item count is zero, getItem() call is invalid");
            } else {
                return mSnapshot.get(index);
            }
        }
        mPagedList.loadAround(index);
        return mPagedList.get(index);
    }

loadAround -> loadAroundInternal,由于loadAroundInternal同样是虚方法,其实现到ContiguousPagedList类,其中的关键方法 schedulePrepend()、scheduleAppend(); 分别是加载已有但是未在界面上展示的数据,加载网络数据,避免了每调用一次getItem,就触发一次请求。

    protected void loadAroundInternal(int index) {
        ...
        if (mPrependItemsRequested > 0) {
            schedulePrepend();
        }

        mAppendItemsRequested = Math.max(appendItems, mAppendItemsRequested);
        if (mAppendItemsRequested > 0) {
            scheduleAppend();
        }
    }

加载更多,即scheduleAppend()。

 @MainThread
    private void scheduleAppend() {
        if (mAppendWorkerRunning) {
            return;
        }
        mAppendWorkerRunning = true;

        final int position = mStorage.getLeadingNullCount()
                + mStorage.getStorageCount() - 1 + mStorage.getPositionOffset();

        // safe to access first item here - mStorage can't be empty if we're appending
        final V item = mStorage.getLastLoadedItem();
        mBackgroundThreadExecutor.execute(new Runnable() {
            @Override
            public void run() {
                if (isDetached()) {
                    return;
                }
                if (mDataSource.isInvalid()) {
                    detach();
                } else {
                    mDataSource.dispatchLoadAfter(position, item, mConfig.pageSize,
                            mMainThreadExecutor, mReceiver);
                }
            }
        });
    }

可见,这里就回到了 mDataSource.dispatchLoadAfter,若使用的PageKeyedDataSource,那就是它是dispatchLoadAfter方法,最终调用到loadAfter(),在其内部实现加载更多的网络请求,从而实现了加载更多。

还有一个疑惑,什么时候可以加载?
看看mAppendWorkerRunning在哪里设置为false的,可以看到在onPageAppended(),它又是由appendPage()里面的callBack回调来的,appendPage()在PageStorage.Receiver对象onPageResult()方法调用的,即mReceiver。
再看mReceiver被谁使用了?PageKeyedDataSource.LoadInitialCallbackImpl、 PageKeyedDataSource.LoadCallbackImpl,这两个类内部都是LoadCallbackHelper对象,被dispatchResultToReceiver()调用,而dispatchResultToReceiver被LoadInitialCallbackImpl和LoadCallbackImpl的onResult()回调。
LoadCallBackImpl的onResult()则是网络请求成功后,调用的。
也就是说,在网络请求成功后,mAppendWorkerRunning就置为false。

loadAroundInternal() 里面有判断,下一次请求必须 mAppendItemsRequested>0,即 index+可展示利用数+1-实际item总数 >0,才进行网络加载。

你可能感兴趣的:(Android Jetpack 之分页:paging)