美文网首页
从 android-paging 开始学习 Paging

从 android-paging 开始学习 Paging

作者: 宝塔山上的猫 | 来源:发表于2019-07-23 10:33 被阅读0次

    谷歌关于 pagign 多数据源示例: https://github.com/googlecodelabs/android-paging
    此示例为 kotlin 语言,使用 room + paging 进行翻页请求

    当然,为了学习方便,我呕心沥血的把 kotlin 转成 java ,如果对对大家有帮助,请在底部赞赏支持一下

    地址:https://github.com/liaozhoubei/Android_Sample

    从 android paging 开始学习

    android paging 流程:

    1.监听 editText ,当发送改变时,使用 LiveData 通知 SearchRepositoriesViewModel 中的 queryLiveData 发生改变
    2.queryLiveData 被 repoResult 所监听,repoResult 中的回调方法

        // 数据源
        DataSource.Factory<Integer, Repo> dataSourceFactory = cache.reposByName(query);
        RepoBoundaryCallback boundaryCallback = new RepoBoundaryCallback(query, service, cache);
        // 网络请求
        MutableLiveData<String> networkErrors = boundaryCallback.getNetworkErrors();
    
        LiveData data = new LivePagedListBuilder(dataSourceFactory, DATABASE_PAGE_SIZE).setBoundaryCallback(boundaryCallback).build();
    

    3.先进行查询数据库的操作,但是查询的结果被包裹在 DataSource.Factory 这个数据源工厂中

    4.然后创建 RepoBoundaryCallback 对象,调用了 GithubRepository 的方法进行网络请求。
    5.RepoBoundaryCallback是 PagedList.BoundaryCallback 的子类,这是个接口类。它有两个方法:

        // PagedList的数据源的初始加载返回零项时调用
        onZeroItemsLoaded() 
        // 当数据源在列表末尾用尽数据时,onItemAtEndLoaded(Object)会调用,
        // 并且您可以启动异步网络加载,将结果直接写入数据库。
        // 由于正在观察数据库,因此绑定到该UI的UI LiveData<PagedList>将
        // 自动更新以考虑新项目。
        onItemAtEndLoaded(@NonNull Repo itemAtEnd)
    

    这两个方法相当重要,就是他们构成了 paging下拉刷新的功能
    6.RepoBoundaryCallback 接口被设置在 LivePagedListBuilder 里面,从此当数据源在末尾时,就调用 onItemAtEndLoaded() 方法,当没有数据时调用 onZeroItemsLoaded() 方法。
    7.在 onItemAtEndLoaded() 中会调用请求网络的方法,请求完网络,就直接插入数据库中

    实际上整个流程已经跑完了,然后运行项目发现不断的刷新数据,不断的进行网络请求,最后将请求到的数据插入数据库中。

    然而令人迷惑的是网络请求完之后,就只见到数据库数据增多了,但是中间是怎么通过LiveData发送新的数据呢?

    奥秘在于 :

        LiveData data = new LivePagedListBuilder(dataSourceFactory, DATABASE_PAGE_SIZE).setBoundaryCallback(boundaryCallback).build();
    

    这行代码中,在这里获取了数据库的数据源,从此 Paging 库每次刷新了数据之后,就在内部直接更新了数据,发送到监听的 Observer 之中。

    这种操作虽然好,但是在开发的时候令人非常的迷惑,因为数据都在内部更新了,完全不清楚它的走向

    paging 中内置的三种 DataSource 的区别

    DataSource.png

    paging 中内置有三种 DataSource ,他们是:

    PositionalDataSource<T>
    ItemKeyedDataSource<Key, Value>
    PageKeyedDataSource<Key, Value>
    

    这三个都是抽象类,使用这几个类作为模板能够实现一个简单的dataSource。
    其中 PositionalDataSource 的父类为 DataSource<Key, Value> ,而 ItemKeyedDataSource 与 PageKeyedDataSource 的父类为 ContiguousDataSource<Key, Value> ,祖父类才为 DataSource<Key, Value>。

    • PositionalDataSource 类用于加载在任意位置请求大小的页面,并提供一个固定的项目计数。这个比较容易理解,以数据库为例,我们从头第一条数据开始取,每次需要取固定10条数据,不断往后面去,也就是第一次取出 0-9 条,第二次取 10-19 条,依次类推。

    PositionalDataSource 就是起着这个作用,给一个取值范围,会每次都按照取值范围顺序取出固定条数的数据

    • ItemKeyedDataSource 用于列表中加载了N条数据,加载下一页数据时,会以列表中最后一条数据的某个字段为Key查询下一页数。它的功能与 PositionalDataSource 类似,都是设置一个固定的条数来取出数据。不同的地方在于 PositionalDataSource 是在请求的时候将当前的请求位置以及请求多少条数据当参数发送,而 ItemKeyedDataSource 则要设置一个 key 来当参数,同时需要后端做支撑。

    同样以每次取10条数据为例。

    ItemKeyedDataSource 初始请求时,同 PositionalDataSource 一样,获取 0 - 9 条数据。但是下拉更新的时候就不一样了,它需要获取一个 key ,这个 key 是数据中的某条唯一字段,然后将 key 以及 请求的条数 当参数进行请求,获取新的数据。

    从后端的角度来说就是获取到一条数据中的唯一字段,然后找到这条数据,查询这条数据中的 位置,最后获取数据当前位置 +1 到后面 10 条数据进行返回

    • PageKeyedDataSource 页面中加载了N条数据,每一页数据都会提供下一页数据的关键字Key作为下次查询的依据,基本与 ItemKeyedDataSource 雷同

    paging 源码解析

    LivePagedListBuilder 初始化

    LivePagedListBuilder(new DataSourceFactory(), config)
                .setBoundaryCallback(null)
                .setFetchExecutor(null)
                .build();
    

    最终会生成一个 ComputableLiveData 对象,即可以检测到生命周期的对象,并且实现 compute 抽象方法,用此抽象方法创建 DataSource 和 PageList。如下图:

    LivePageDListBuilder.png

    图片来源:https://blog.csdn.net/Alexwll/article/details/83246201

    compute() 方法会在 LiveData 的 onActive() 方法中进行调用,实际是ObserverWrapper的activeStateChanged()方法中调用 onActive()。

    我们在仔细看 compute() 方法

     protected PagedList<Value> compute() {
         ...
    
         do {
             ...
             mDataSource = dataSourceFactory.create();
             mDataSource.addInvalidatedCallback(mCallback);
    
             mList = new PagedList.Builder<>(mDataSource, config)
                     .setNotifyExecutor(notifyExecutor)
                     .setFetchExecutor(fetchExecutor)
                     .setBoundaryCallback(boundaryCallback)
                     .setInitialKey(initializeKey)
                     .build();
         } while (mList.isDetached());
         return mList;
     }
    

    首先是获取 DataSource 对象,在 LivePagedListBuilder.build 的时候传入了 DataSourceFactory ,在这里回调DataSourceFactory.create(), 创建具体的 DataSource。

    其次创建了 PagedList 对象,并且将 DataSource 传入 。PagedList是 paging 中要具操控中的对象。

    至此,Paging 完成了第一步,数据源与数据列表的创建与绑定

    PagedList 源码

    PagedList 继承结构图:


    AbstractList.png

    上面在创建 PagedList 的时候使用了 Builder,实质上是调用了 PagedList.create() 方法,代码如下:

    static <K, T> PagedList<T> create(@NonNull DataSource<K, T> dataSource,
            @NonNull Executor notifyExecutor,
            @NonNull Executor fetchExecutor,
            @Nullable BoundaryCallback<T> boundaryCallback,
            @NonNull Config config,
            @Nullable K key) {
        if (dataSource.isContiguous() || !config.enablePlaceholders) {
            int lastLoad = ContiguousPagedList.LAST_LOAD_UNSPECIFIED;
            if (!dataSource.isContiguous()) {
                //noinspection unchecked
                dataSource = (DataSource<K, T>) ((PositionalDataSource<T>) dataSource)
                        .wrapAsContiguousWithoutPlaceholders();
                if (key != null) {
                    lastLoad = (Integer) key;
                }
            }
            ContiguousDataSource<K, T> contigDataSource = (ContiguousDataSource<K, T>) dataSource;
            return new ContiguousPagedList<>(contigDataSource,
                    notifyExecutor,
                    fetchExecutor,
                    boundaryCallback,
                    config,
                    key,
                    lastLoad);
        } else {
            return new TiledPagedList<>((PositionalDataSource<T>) dataSource,
                    notifyExecutor,
                    fetchExecutor,
                    boundaryCallback,
                    config,
                    (key != null) ? (Integer) key : 0);
        }
    }
    

    上面使用 dataSource.isContiguous() 以及 config.enablePlaceholders 判断是创建 ContiguousPagedList 还是 TiledPagedList,它们的意义如下:

    dataSource.isContiguous() : 如果数据源保证生成一组连续的项,则返回true,而不会生成空白。
    
    config.enablePlaceholders : 定义PagedList是否可以显示空占位符(如果DataSource提供它们)。
    

    进入 ContiguousPagedList 构造方法 :

    ContiguousPagedList(
            @NonNull ContiguousDataSource<K, V> dataSource,
            @NonNull Executor mainThreadExecutor,
            @NonNull Executor backgroundThreadExecutor,
            @Nullable BoundaryCallback<V> boundaryCallback,
            @NonNull Config config,
            final @Nullable K key,
            int lastLoad) {
    
        super(new PagedStorage<V>(), mainThreadExecutor, backgroundThreadExecutor,
                boundaryCallback, config);
        mDataSource = dataSource;
        mLastLoad = lastLoad;
        // 当数据源无效时
        if (mDataSource.isInvalid()) {
            detach();
        } else {
            mDataSource.dispatchLoadInitial(key,
                    mConfig.initialLoadSizeHint,
                    mConfig.pageSize,
                    mConfig.enablePlaceholders,
                    mMainThreadExecutor,
                    mReceiver);
        }
        mShouldTrim = mDataSource.supportsPageDropping()
                && mConfig.maxSize != Config.MAX_SIZE_UNBOUNDED;
    }
    

    首先看到这行代码:

    super(new PagedStorage<V>(), mainThreadExecutor, backgroundThreadExecutor,
                boundaryCallback, config);
    

    这里创建了 PagedStorage 对象,这个对象是保持页面数据的实际对象,实则在里面用 ArrayList 来实现各种增删改查的操作,这个后面再说。

    然后调用了 mDataSource.dispatchLoadInitial() 方法,从名字上就可以看出它是处理加载初始化的方法,跟进此方法,发现它是 ContiguousDataSource.dispatchLoadInitial() ,由 ItemKeyedDataSource 以及 PageKeyedDataSource 进行实现。最后调用 loadInitial() 这个由我们具体实现的获取初始化数据的方法,获取数据后,通过接口回调的方式通知主线程,最后通过 LiveData 的 observe()接口返回到需要数据的 Adapter 中。

    以上就完成了整个 Paging 的初始化调用。

    初始化示例代码如下:

        PagedList.Config config = new PagedList.Config.Builder()
                .setPageSize(10)                         //配置分页加载的数量
                .setEnablePlaceholders(false)     //配置是否启动PlaceHolders
                .setInitialLoadSizeHint(10)              //初始化加载的数量
                .build();
    
        LiveData<PagedList<DataBean>> liveData = new LivePagedListBuilder(
            new MyDataSourceFactory(), config).build();
    
        liveData.observe(this,new Observer<PagedList<DataBean>>() {
            @Override
            public void onChanged(@Nullable PagedList<DataBean> dataBeans) {
                // 每次数据更改后都从此接口获取数据
                mAdapter.submitList(dataBeans);
            }
        });
    

    数据上拉与下拉刷新

    我们为什么要用 Paging 这个库,为的就是更便捷的上拉刷新,所以我们必须得清楚它的上拉刷新机制。

    我们跟踪一下 PositionalDataSource 上拉时的表现,在上拉的时候,它会调用 loadRange() 方法,所以我们一步步更上去看它的调用链。

    最后发现它的调用者为 PagedListAdapter 中的 getItem(int position) 方法,详情如下:

    public T getItem(int index) {
        ...
        mPagedList.loadAround(index);
        return mPagedList.get(index);
    }
    

    这里的 index 实际上是界面上显示的最后一条数据的位置,同时这最后一条数据也是这条数据在已保存的列表中的位置。Paging 会将获取到的数据全部保持到一个 ArrayList 之中。

    loadRange() 又调用了 loadAround() 方法,然后 loadAround() 又调用了抽象方法 loadAroundInternal(index),如下

    public void loadAround(int index) {
        if (index < 0 || index >= size()) {
            throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size());
        }
    
        mLastLoad = index + getPositionOffset();
        // 具体加载数据方法
        loadAroundInternal(index);
    
        mLowestIndexAccessed = Math.min(mLowestIndexAccessed, index);
        mHighestIndexAccessed = Math.max(mHighestIndexAccessed, index);
    
        tryDispatchBoundaryCallbacks(true);
    }
    

    loadAroundInternal(index) 方法是在 TiledPagedList 与 ContiguousPagedList 中会执行不同逻辑。

    TiledPagedList 如下:

    @Override
    protected void loadAroundInternal(int index) {
        mStorage.allocatePlaceholders(index, mConfig.prefetchDistance, mConfig.pageSize, this);
    }
    

    mStorage.allocatePlaceholders 又是什么呢?它是一个 PagedStorage ,用于存储分片数据的类,内部是由一系列 List 的增删改查操作实现的, allocatePlaceholders() 方法如下:

    public void allocatePlaceholders(int index, int prefetchDistance,
                                     int pageSize, Callback callback) {
        if (pageSize != mPageSize) {
            if (pageSize < mPageSize) {
                throw new IllegalArgumentException("Page size cannot be reduced");
            }
            if (mPages.size() != 1 || mTrailingNullCount != 0) {
                // not in single, last page allocated case - can't change page size
                throw new IllegalArgumentException(
                        "Page size can change only if last page is only one present");
            }
            mPageSize = pageSize;
        }
    
        final int maxPageCount = (size() + mPageSize - 1) / mPageSize;
        int minimumPage = Math.max((index - prefetchDistance) / mPageSize, 0);
        int maximumPage = Math.min((index + prefetchDistance) / mPageSize, maxPageCount - 1);
    
        allocatePageRange(minimumPage, maximumPage);
        int leadingNullPages = mLeadingNullCount / mPageSize;
        for (int pageIndex = minimumPage; pageIndex <= maximumPage; pageIndex++) {
            int localPageIndex = pageIndex - leadingNullPages;
            if (mPages.get(localPageIndex) == null) {
                //noinspection unchecked
                mPages.set(localPageIndex, PLACEHOLDER_LIST);
                callback.onPagePlaceholderInserted(pageIndex);
            }
        }
    }
    

    这片代码的逻辑也很简单,只有在 mPages.get(localPageIndex) 也就是下一页分片不存在的时候调用 callback.onPagePlaceholderInserted(pageIndex) ,此方法会在线程中调用 PositionalDataSource 的 loadRange() 方法。另外 onPagePlaceholderInserted() 方法只在 TiledPagedList 中实现, ContiguousPagedList中执行会抛出异常

    ContiguousPagedList 如下:

    @MainThread
    @Override
    protected void loadAroundInternal(int index) {
        int prependItems = getPrependItemsRequested(mConfig.prefetchDistance, index,
                mStorage.getLeadingNullCount());
        int appendItems = getAppendItemsRequested(mConfig.prefetchDistance, index,
                mStorage.getLeadingNullCount() + mStorage.getStorageCount());
    
        mPrependItemsRequested = Math.max(prependItems, mPrependItemsRequested);
        if (mPrependItemsRequested > 0) {
            schedulePrepend();
        }
    
        mAppendItemsRequested = Math.max(appendItems, mAppendItemsRequested);
        if (mAppendItemsRequested > 0) {
            scheduleAppend();
        }
    }
    

    其中有两个重要的变量

    mPrependItemsRequested :向前需要多少条数据,当其 >0 时,会调用 DataSource(ItemKeyedDataSource与PageKeyedDataSource) 的 loadBefore() 方法(PositionalDataSource 则调用 dispatchLoadRange() 方法)
    mAppendItemsRequested : 向后需要多少条数据,当其 >0 时,会调用 DataSource 的 loadAfter() 方法
    

    mPrependItemsRequested 的值为获取自己与 prependItems 之间的最大值 ,mAppendItemsRequested 则是获取自己与 appendItems 的最大值。prependItems 与 appendItems 则是通过以下两个方法计算出来的。

    static int getPrependItemsRequested(int prefetchDistance, int index, int leadingNulls) {
        return prefetchDistance - (index - leadingNulls);
    }
    
    static int getAppendItemsRequested(
            int prefetchDistance, int index, int itemsBeforeTrailingNulls) {
        return index + prefetchDistance + 1 - itemsBeforeTrailingNulls;
    }
    

    方法中第一个参数 prefetchDistance ,一般是设置 PagedList.Config 时的 PageSize , 其含义为:预取距离,用于定义加载前的距离。如果此值设置为50,则分页列表将尝试提前加载50个项目已经访问过的数据。

    第二个参数 index 则是当前 item 所处的位置,上面有提及。

    第三个参数 leadingNulls 和 itemsBeforeTrailingNulls 表示列表中为 null 的数据的个数,可不理会。

    现在我们就知道上拉和下拉的逻辑了。假如我们设置每页的数据需要 10 条,当我们把页面往上拖动,需要加载更多数据,这时当前数据存量为10条,此时页面底部的 index 为 10,拖拽时,appendItems = 10(index)+ 10(mConfig.prefetchDistance) - 10(mStorage.getStorageCount() 当前数据存量),最后得到结果为 10,这时就需要请求网络加载更多。

    加载完毕后在 onPageAppended() 方法更新 mAppendItemsRequested 的值,如下:

    @MainThread
    @Override
    public void onPageAppended(int endPosition, int changedCount, int addedCount) {
        // consider whether to post more work, now that a page is fully appended
        mAppendItemsRequested = mAppendItemsRequested - changedCount - addedCount;
        mAppendWorkerState = READY_TO_FETCH;
        if (mAppendItemsRequested > 0) {
            // not done appending, keep going
            scheduleAppend();
        }
    
        // finally dispatch callbacks, after append may have already been scheduled
        notifyChanged(endPosition, changedCount);
        notifyInserted(endPosition + changedCount, addedCount);
    }
    

    如此便完成了一个闭环。

    同理,下拉刷新时也是同样进行更新数据,当界面中最顶上的 index 小于预设的 PageSize 的时候,就会触发 loadBefore() ,最后在 PagedStroage 将加载出来的数据加到 ArrayList 的最顶上。

    PagedList.Config 配置信息

    PagedList.Config 的配置参数会影响到 Paging 的 DataSource 的使用,以下稍微解释一下这些参数:

    setEnablePlaceholders(boolean) : 此选项会影响到具体初始化那种 PageList,而不同的 PageList 会影响到是否要调用 BoundaryCallback 这个监听数据是否到达边界的回调。

    当设置为 true 的时候,会将数据源初始化为 PositionalDataSource ,为fasle 时初始化为:ContiguousDataSource 。也就是说实例化 DataSource 的时候并不一定取决于我们设置了哪种 DataSource ,而是取决于 PagedList.Config 的参数。 PagedList 初始化 DataSource 代码如下(PagedList.create()):

            PagedList<T> create(@NonNull DataSource<K, T> dataSource,
                @NonNull Executor notifyExecutor,
                @NonNull Executor fetchExecutor,
                @Nullable BoundaryCallback<T> boundaryCallback,
                @NonNull Config config,
                @Nullable K key) {
            if (dataSource.isContiguous() || !config.enablePlaceholders) {
                if (!dataSource.isContiguous()) {
                    //noinspection unchecked
                    dataSource = (DataSource<K, T>) ((PositionalDataSource<T>) dataSource)
                            .wrapAsContiguousWithoutPlaceholders();
                    if (key != null) {
                        lastLoad = (Integer) key;
                    }
                }
                ContiguousDataSource<K, T> contigDataSource = (ContiguousDataSource<K, T>) dataSource;
                return new ContiguousPagedList<>(contigDataSource,
                        notifyExecutor,
                        fetchExecutor,
                        boundaryCallback,
                        config,
                        key,
                        lastLoad);
            } else {
                return new TiledPagedList<>((PositionalDataSource<T>) dataSource,
                        notifyExecutor,
                        fetchExecutor,
                        boundaryCallback,
                        config,
                        (key != null) ? (Integer) key : 0);
            }
    

    从中可看出仅有 dataSource.isContiguous() 为 ture 以及 config.enablePlaceholders 为 false 时才会初始化 ContiguousPagedList ,其余的时候都初始化 TiledPagedList。

    其次,config.enablePlaceholders 的默认值为 ture, 也就是说默认情况下初始化的都是 TiledPagedList 。

    在实际测试中,config.enablePlaceholders 为 false, BoundaryCallback 为 null 时, 并且 DataSource 为 PositionDataSource 的时候,此时界面中可正常加载数据。

    但当 config.enablePlaceholders 为 true 时,却发现界面只加载了第一份数据,不在继续往下加载。

    究其原因:
    PositionalDataSource 在上拉的时候,是调用子类的 landRange 方法,

    当 config.enablePlaceholders 为 true 时,会初始化PositionalDataSource,同时 PageList 初始化为 TiledPagedList。在 TiledPagedList 的 loadAroundInternal 上拉刷新中,此时会调用 PagedStorag.allocatePlaceholders 方法,在这个方法中判断是否要调用 PositionalDataSource.landRange,然而 mPages.get(localPageIndex) 不会为 null, 导致landRange不被调用。如下(位于 PagedStorag.allocatePlaceholders() ):

        public void allocatePlaceholders(int index, int prefetchDistance,
                                         int pageSize, Callback callback) {
        ....
            for (int pageIndex = minimumPage; pageIndex <= maximumPage; pageIndex++) {
                int localPageIndex = pageIndex - leadingNullPages;
                if (mPages.get(localPageIndex) == null) {
                    Log.e("PagedStorage", "mPages.get(localPageIndex) == null" );
                    //noinspection unchecked
                    mPages.set(localPageIndex, PLACEHOLDER_LIST);
                    callback.onPagePlaceholderInserted(pageIndex);
                }
            }
        }
    

    ContiguousDataSource 在上拉的时候是调用 loadAfter 方法。
    当 config.enablePlaceholders 为 false 时,会初始化 ContiguousWithoutPlaceholdersWrapper, 将 会初始化PositionalDataSource 包装为 ContiguousDataSource, 同时 PageList 初始化为 ContiguousPagedList。在ContiguousPagedList 的上拉刷新 loadAroundInternal 中调用scheduleAppend, 然后调用 ContiguousDataSource。dispatchLoadAfter,接着调用 ContiguousWithoutPlaceholdersWrapper.dispatchLoadAfter,最后调用到 PositionalDataSource.landRange 刷新数据。

    dataSource.isContiguous() 是 dataSource 的抽象方法,由子类赋值, PositionDataSource / TitledDataSource 为 false, ContiguousDataSource 为 true . 同时 PagedList 中也有此参数,但是与 DataSource 中保持一致。

    BoundaryCallback

    // 如果设置为true,则mBoundaryCallback为非null,并且应在附近加载时调度
    private boolean mBoundaryCallbackBeginDeferred = false;
    private boolean mBoundaryCallbackEndDeferred = false;   
    
    // loadAround访问的最低和最高索引。 用于决定何时应调度mBoundaryCallback
    private int mLowestIndexAccessed = Integer.MAX_VALUE;
    private int mHighestIndexAccessed = Integer.MIN_VALUE;
    
    
    // 此方法在 DataSource 的 loadInitial() / loadRange() 调用后,调用 onResult(@NonNull List<T> data) 时调用
    deferBoundaryCallbacks(final boolean deferEmpty,
                final boolean deferBegin, final boolean deferEnd)
    
    // mLowestIndexAccessed / mHighestIndexAccessed已更新,因此请检查是否需要调度边界回调。 边界回调延迟到最后一项加载,并且访问发生在边界附近。
    // 注意:我们在此处发布,因为RecyclerView可能希望在响应中添加项目,并且此调用发生在PagedListAdapter bind中。
    tryDispatchBoundaryCallbacks(true);
    

    关于 BoundaryCallback 的理解还不够透彻,目前有个疑惑就是在单数据源的时候,上拉到底部不会调用 BoundaryCallback.onItemAtEndLoaded() 方法,而是调用 DataSource 的加载更多的方法。目前还没有研究透彻,先搁置一边吧。

    相关文章

      网友评论

          本文标题:从 android-paging 开始学习 Paging

          本文链接:https://www.haomeiwen.com/subject/ewznlctx.html