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 <T> void dispatchDiff(ListUpdateCallback callback,
            final PagedStorage<T> oldList,
            final PagedStorage<T> 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,才进行网络加载。

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

推荐阅读更多精彩内容