Paging 使用及遇到的问题

2,595 阅读4分钟

Paging

概述

Jetpack 的一个分页库,帮助开发者更好的分离ui和数据获取的逻辑,降低项目的耦合。本文主要描述从服务器直接获取数据

项目地址:github.com/Tkorn/kotli…

库架构

  • DataSource.Factory, 顾名思义,就是数据源工厂。抽象类,需要我们实现create() 方法,返回DataSource 对象。**注意:create() 每次被调用都应该返回新对象,**不然调用invalidate() 时不仅不能刷新列表还会出现死循环。(原因后面再解释)
    fun createDataSourceFactory(dataSource: DataSource<Long, UserBean>):DataSource.Factory<Long, UserBean>{
        return object: DataSource.Factory<Long,UserBean>(){
            override fun create(): DataSource<Long, UserBean> {
                return dataSource
            }
        }
    }
  • DataSource, Paging已经帮我们提供了三个非常全面的实现,分别是:

    • PageKeyedDataSource: 通过当前页相关的key来获取数据,非常常见的是key作为请求的page的大小。
    • ItemKeyedDataSource: 通过具体item数据作为key,来获取下一页数据。例如聊天会话,请求下一页数据可能需要上一条数据的id。
    • PositionalDataSource: 通过在数据中的position作为key,来获取下一页数据。这个典型的就是上面所说的在Database中的运用。

    这里我们只介绍一下 ItemKeyedDataSource 的使用。 ItemKeyedDataSource 同样是一个抽象类,需要我们实现以下四个方法:

    • loadInitial(LoadInitialParams params,LoadInitialCallback callback) 加载初始(第一页)数据
    • loadAfter(LoadParams params,LoadCallback callback) 加载下一页数据
    • loadBefore(LoadParams params,LoadCallback callback) 加载上一页数据,一般用在新闻资讯类列表,把请求的数据加载到列表前面。不需要就空着
    • Key getKey(Value item)
  • PagedList, 分页库的关键组件是 PagedList 类,用于加载应用数据块或页面。随着所需数据的增多,系统会将其分页到现有的 PagedList 对象中。如果任何已加载的数据发生更改,会从 LiveData 或基于 RxJava2 的对象向可观察数据存储器发出一个新的 PagedList 实例。随着 PagedList 对象的生成,应用界面会呈现其内容,同时还会考虑界面控件的生命周期。(官方翻译) 可以通过 LivePagedListBuilder 创建

    val userLiveData =
        LivePagedListBuilder(mRepository.createDataSourceFactory(createDataSource()),
            mRepository.createConfig())
            .setInitialLoadKey(1)
            .build()

LivePagedListBuilder(DataSource.Factory<Key, Value> dataSourceFactory, PagedList.Config config)

LivePagedListBuilder 需要DataSource.Factory(上面介绍了)、PagedList.Config(提供分页需要的参数)

Config(
    int pageSize, //一页加载多少数据
    int prefetchDistance,//加载到第几条时请求下一页数据
    boolean enablePlaceholders, 是否使用占位符
    int initialLoadSizeHint, //第一次加载多少数据
    int maxSize //最多保存多少数据
){...}
  • PagedListAdapter, 与RecyclerView.Adapter的使用区别不大,只是对getItemCount与getItem进行了重写,因为它使用到了DiffUtil,避免对数据的无用更新。
class PagingAdapter : PagedListAdapter<ArticleModel, PagingVH>(diffCallbacks) {
 
    companion object {
        private val diffCallbacks = object : DiffUtil.ItemCallback<ArticleModel>() {

            override fun areItemsTheSame(oldItem: ArticleModel, newItem: ArticleModel): Boolean = oldItem.id == newItem.id
 
            override fun areContentsTheSame(oldItem: ArticleModel, newItem: ArticleModel): Boolean = oldItem == newItem

        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PagingVH = PagingVH(R.layout.item_paging_article_layout, parent)
 
    override fun onBindViewHolder(holder: PagingVH, position: Int) = holder.bind(getItem(position))
}

这样adapter也已经构建完成,最后一旦PagedList被观察到,使用submitList传入到adapter即可。

viewModel.userList.observe(this, Observer {
    adapter.submitList(it)
})

详细使用可以看看我的Demo,使用了mvvm + LiveData + Koin + 协程 + Retrofit。项目地址在最上面

问题

  • 为什么 DataSource.Factory 的create() 每次调用需要返回新对象 因为当 invalidate() 调用时 DataSource 的 mInvalid 会被设置为 true,然后调用onInvalidated()
    //源码
    @AnyThread
    public void invalidate() {
        if (mInvalid.compareAndSet(false, true)) {
            for (InvalidatedCallback callback : mOnInvalidatedCallbacks) {
                callback.onInvalidated();
            }
        }
    }
    

LivePagedListBuilder 的create() 重写了 DataSource.InvalidatedCallback

//LivePagedListBuilder 源码    
    @AnyThread
    @NonNull
    @SuppressLint("RestrictedApi")
    private static <Key, Value> LiveData<PagedList<Value>> create(
            @Nullable final Key initialLoadKey,
            @NonNull final PagedList.Config config,
            @Nullable final PagedList.BoundaryCallback boundaryCallback,
            @NonNull final DataSource.Factory<Key, Value> dataSourceFactory,
            @NonNull final Executor notifyExecutor,
            @NonNull final Executor fetchExecutor) {
        return new ComputableLiveData<PagedList<Value>>(fetchExecutor) {
            @Nullable
            private PagedList<Value> mList;
            @Nullable
            private DataSource<Key, Value> mDataSource;

            private final DataSource.InvalidatedCallback mCallback =
                    new DataSource.InvalidatedCallback() {
                        @Override
                        public void onInvalidated() {
                            invalidate();
                        }
                    };

            @SuppressWarnings("unchecked") // for casting getLastKey to Key
            @Override
            protected PagedList<Value> compute() {
                @Nullable Key initializeKey = initialLoadKey;
                if (mList != null) {
                    initializeKey = (Key) mList.getLastKey();
                }

                do {
                    if (mDataSource != null) {
                        mDataSource.removeInvalidatedCallback(mCallback);
                    }

                    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;
            }
        }.getLiveData();
    }
}

可以看到当 mList.isDetached() = true 时就会进入死循环。它的默认值是为false 的。在do 里面 mDataSource = dataSourceFactory.create(); mDataSource是重新获取了的,然后通过 PagedList.Builder 去获取新的 mList 数据。

    @WorkerThread
    TiledPagedList(@NonNull PositionalDataSource<T> dataSource,
            @NonNull Executor mainThreadExecutor,
            @NonNull Executor backgroundThreadExecutor,
            @Nullable BoundaryCallback<T> boundaryCallback,
            @NonNull Config config,
            int position) {
        super(new PagedStorage<T>(), mainThreadExecutor, backgroundThreadExecutor,
                boundaryCallback, config);
        mDataSource = dataSource;

        final int pageSize = mConfig.pageSize;
        mLastLoad = position;

        if (mDataSource.isInvalid()) {
            detach();
        } else {
            final int firstLoadSize =
                    (Math.max(mConfig.initialLoadSizeHint / pageSize, 2)) * pageSize;

            final int idealStart = position - firstLoadSize / 2;
            final int roundedPageStart = Math.max(0, idealStart / pageSize * pageSize);

            mDataSource.dispatchLoadInitial(true, roundedPageStart, firstLoadSize,
                    pageSize, mMainThreadExecutor, mReceiver);
        }
    }

获取新数据前是会判断 mDataSource.isInvalid() ,这时如果我们重新的DataSource的create() 的方法返回的不是新对象,而是之前的对象,这个mDataSource已经调用invalidate() 因此就会走到 detach() 方法,不获取新数据。

//源码
    @SuppressWarnings("WeakerAccess")
    public void detach() {
        mDetached.set(true);
    }

可以看到 detach() 会把mDetached 这位true,这就会导致mList.isDetached() 获取的时候是true,LivePagedListBuilder 的 create() 方法就会死循环。