Jetpack-Android Paging Library

目录:

需求来源
Paging运作流程
Paging三大部件
如何使用
三种DataSource对比
LivePagedListBuilder、RxPagedListBuilder对比

一、需求来源:

为方便实现上拉加载、简化代码、上拉加载逻辑可配置...总之就是为了方便

Paging出现前,上拉加载触发一般是通过:

  1. 监听RecyclerView的滚动事件,判断RecyclerView是否滚动到底部
  2. 处理Adapter的onBindViewHolder方法,根据位置与数量判断当前位置item是否该触发上拉加载
  3. 或者其他方式...

如方式一:

        recycler_view.addOnScrollListener(object : RecyclerView.OnScrollListener() {

            var shouldReload = false

            override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
                if (newState == RecyclerView.SCROLL_STATE_IDLE && shouldReload) {
                    // 加载下一页
                    ...
                }
            }

            override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
                val layoutManager: LinearLayoutManager =
                    recycler_view.layoutManager as LinearLayoutManager
                layoutManager.apply {

                    val firstVisibleItem = findFirstVisibleItemPosition()
                    val visibleItemCount = childCount
                    val totalItemCount = itemCount
                    shouldReload = firstVisibleItem + visibleItemCount == totalItemCount && dy > 0
                }
            }
        })

BaseRecyclerViewAdapterHelper库实现方式为方案二,部分代码如下:

    @Override
    public void onBindViewHolder(@NonNull K holder, int position) {
        ...  // 其他代码
        autoLoadMore(position);
        ...  // 其他代码
    }

    @Override
    public void onBindViewHolder(@NonNull K holder, int position, @NonNull List<Object> payloads) {
        ...  // 其他代码
        autoLoadMore(position);
        ...  // 其他代码
    }

    // 自动上拉加载判断逻辑
    private void autoLoadMore(int position) {

        // mPreLoadNumber为预加载控制数量,默认为1
        if (position < getItemCount() - mPreLoadNumber) {
            return;
        }
        if (mLoadMoreView.getLoadMoreStatus() != LoadMoreView.STATUS_DEFAULT) {
            return;
        }
        
        ... // 其他代码
        mRequestLoadMoreListener.onLoadMoreRequested();
        ... // 其他代码
    }

Paging运作流程

首先,Adapter会继承自PagedListAdapter(DiffUtils)PagedListAdapter(DiffUtils)最终也是继承自RecyclerView.Adapter。执行到onBindViewHolder()数据绑定时,调用getItem(position)获取当前位置的数据,此时PagedListAdapter就会知道列表现在处于什么位置,以及是否触发加载下一页功能。当加载下一页功能被触发时,会通知内部PagedListDataSource拉取数据,获得数据后通过DiffUtils对比得到最终数据集

Paging.png

Paging三大部件

1. PagedListAdapter

继承自RecyclerView.Adapter,用来承载PagedList。PagedListAdapter构造函数需提供DiffUtil.ItemCallback对象,或者是AsyncDifferConfig对象。

    protected PagedListAdapter(@NonNull DiffUtil.ItemCallback<T> diffCallback) {
        mDiffer = new AsyncPagedListDiffer<>(this, diffCallback);
        mDiffer.addPagedListListener(mListener);
    }

    protected PagedListAdapter(@NonNull AsyncDifferConfig<T> config) {
        mDiffer = new AsyncPagedListDiffer<>(new AdapterListUpdateCallback(this), config);
        mDiffer.addPagedListListener(mListener);
    }

数据对比主要是用实例化的AsyncDifferConfig对象,其构造方法如下:

    public AsyncPagedListDiffer(@NonNull RecyclerView.Adapter adapter,
            @NonNull DiffUtil.ItemCallback<T> diffCallback) {
        mUpdateCallback = new AdapterListUpdateCallback(adapter);
        mConfig = new AsyncDifferConfig.Builder<>(diffCallback).build();
    }

    @SuppressWarnings("WeakerAccess")
    public AsyncPagedListDiffer(@NonNull ListUpdateCallback listUpdateCallback,
            @NonNull AsyncDifferConfig<T> config) {
        mUpdateCallback = listUpdateCallback;
        mConfig = config;
    }

2. PagedList

PagedList是一个抽象类,是一个List的封装类别,与我们熟悉的ArrayList一样继承自AbstractList。用来存储分页载入的数据集合,并且通知DataSource数据加载时机。可以通过PagedList.Config配置

(1). mPageSize:页面大小即每次加载时加载的数量
(2). mPrefetDistance:预取距离,给定UI中最后一个可见的Item,超过这个Item应该预取一段数据

PagingConfig.png

3. DataSource

负责实现数据载入实现,常用子类如下:

  1. PageKeyedDataSource:当后一页的取得方式从当前页得知,通过当前页相关的key来获取数据。

假定一个场景帮助理解:浏览短视频数据时返回的每个视频携带视频类别,当哪个视频观看比较久,则下一页优先获取该类别的视频--后一页的请求,根据前一页的key(视频类别)获取
简单常见的按照页序号查询数据也可以用这个DataSource。初始时callback设置前后页key为-1,1。加载下一页设置callback的key递增1,加载上一页设置callback的key递减1.

  1. PositionalDataSource:通过在数据中的position作为key来获取下一页数据。数据排一排,根据配置的起始位置和获取数量,获取从指定位置开始后续n条数据。比如说,请求返回从第100条数据开始的之后20条数据。

假如设置初始请求从第30项开始,获取15条数据。此时下一页请求参数为从第45项开始,获取15条数据。参数中startPosition=requestedStartPosition+pageSize

  1. ItemKeyedDataSource:通过当前页数据信息(具体的item)作为key,来获取下一页数据。必须由当前页数据去加载下一页数据。

场景:例如小红书、资讯类的app,上下页的请求参数都是从当前页配置的

如何使用

1. 定义DiffCallback

boolean areItemsTheSame(@NonNull DataBean oldItem, @NonNull DataBean newItem):判断是不是同一个Item
boolean areContentsTheSame(@NonNull DataBean oldItem, @NonNull DataBean newItem):判断两个Item内容是否相同,当areItemsTheSame为true时才调用

2. 初始化适配器,绑定RecyclerView

使用基本与RecyclerView.Adapter一样

差异点:需传入DiffUtil.ItemCallback。获取数据使用getItem(position)方法

    private class MyAdapter extends PagedListAdapter<DataBean,  ViewHolder> {
        public MyAdapter() {
            super(mDiffCallback);
        }

        ... // 其他代码

        @Override
        public void onBindViewHolder(MyViewHolder holder, int position) {
            DataBean data = getItem(position);
            ... // 填充Item
        }
    }
3. 设置PagedList.Config
        PagedList.Config mPagedListConfig = new PagedList.Config.Builder()
                .setPageSize(2)                 // 设置后续页面(非第一页)每页加载的数量
                .setPrefetchDistance(2)         // getItem(position)时调用
                .setEnablePlaceholders(true)    // 设置占位符,默认true
                .setInitialLoadSizeHint(3)      // 设置第一页加载的数量
                .build();
4. 设置DataSource
    private class MyDataSource extends PageKeyedDataSource<Integer, DataBean> {

        @Override
        public void loadInitial(@NonNull final LoadInitialParams<Integer> params,
                                @NonNull final LoadInitialCallback<Integer, DataBean> callback) {
             // 请求第一页数据
        }

        @Override
        public void loadBefore(@NonNull LoadParams<Integer> params, @NonNull LoadCallback<Integer, DataBean> callback) {

        }

        @Override
        public void loadAfter(@NonNull final LoadParams<Integer> params,
                              @NonNull final LoadCallback<Integer, DataBean> callback) {
             // 请求下一页数据
        }
    }
5. 设置DataSourceFactory
    private class DataSourceFactory extends DataSource.Factory<Integer, DataBean> {

        @NonNull
        @Override
        public DataSource<Integer, DataBean> create() {
            return new MyDataSource();
        }
    }
6. 设置LiveData
        LivePagedListBuilder<Integer, DataBean> builder = new LivePagedListBuilder<>(
                new DataSourceFactory(), mPagedListConfig);
        LiveData<PagedList<DataBean>> mPagedList = builder.build();
        mPagedList.observe(this, new Observer<PagedList<DataBean>>() {

            @Override
            public void onChanged(PagedList<DataBean> o) {
                // 填充数据  
                mAdapter.submitList(o);
            }
        });

三种DataSource对比

1. PositionalDataSource:根据列表的绝对位置获取数据
/**
 * desc:根据列表的绝对位置决定放数据</br>
 * 从任意指定位置开始获取数据
 * time: 2019/11/12-11:25</br>
 * author:Leo </br>
 */
注:其中的泛型Item是请求到列表每项数据的实体类
class TestPositionalDataSource : PositionalDataSource<Item>() {

    override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback<Item>) {
        params.apply {
            val items = getIncreaseItems(startPosition, loadSize)
            callback.onResult(items)
        }
    }

    override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback<Item>) {
        params.apply {
            val items = getIncreaseItems(requestedStartPosition, pageSize)
            callback.onResult(items, 0)
        }
    }
}

该方式的关键点在于LoadInitialParamsLoadRangeParams中参数的获取

ContiguousPagedList.java
175行 ContiguousPagedList构造函数
mDataSource.dispatchLoadInitial(key,
     mConfig.initialLoadSizeHint,
     mConfig.pageSize,
     mConfig.enablePlaceholders,
     mMainThreadExecutor,
     mReceiver);
  • LoadInitialParams各参数与PagedList.Config对应关系如下:
    // key必须为Integer类型
    requestedStartPosition -> LivePagedListBuilder.setInitialLoadKey
    requestedLoadSize -> config.initialLoadSizeHint
    pageSize -> config.pageSize
    placeholdersEnabled -> false // 该模式下写死为false
    PositionalDataSource.java

    final void dispatchLoadInitial(...) {
        ...
        LoadInitialParams params = new LoadInitialParams(
                requestedStartPosition, requestedLoadSize, pageSize, acceptCount);
        loadInitial(params, callback);
        ...
    }

初次加载数据时,参数值读取的参数如上构造函数`LoadInitialParams`
注意:此时placeholdersEnabled实际为false

LoadRangeParams参数值来源

  • 取下一页 dispatchLoadAfter
    startPosition -> 当前位置+1
    loadSize -> config.pageSize
  • 取上一页 dispatchLoadBefore
    参数值根据不同情景赋值,源码如下
PositionalDataSource.java
@Override
void dispatchLoadBefore(...) {
    int startIndex = currentBeginIndex - 1;
    if (startIndex < 0) {
         // 列表为空 情景一
         mSource.dispatchLoadRange(
               PageResult.PREPEND, startIndex, 0, mainThreadExecutor,receiver);
    } else {
         // 列表不为空 情景二
         int loadSize = Math.min(pageSize, startIndex + 1);
         startIndex = startIndex - loadSize + 1;
         mSource.dispatchLoadRange(
               PageResult.PREPEND, startIndex, loadSize, mainThreadExecutor, receiver);
    }
}

LoadRangeParams参数值来源
情景一:此时列表为空,此时
    startPosition -> -1 
    loadSize -> 0
情景二:正常加载之前数据
    startPosition -> 当前位置向前推loadSize个数
    loadSize -> 当前位置与config.pageSize的最小者(避免可能出现当前位置之前的数量少于pageSize情况)

总结:初始化加载时,参数从PagedList配置项读取,无占位符。加载前后数据时,开始位置根据当前位置向前/后推算,加载数量读取自config.pageSize。该类型DataSource无法加载上一页

  • 此时获取上页/下页数据仅仅根据位置序号就能获得。

使用注意点:
使用PositionalDataSource时,需注意需设置config.enablePlaceholders=false,否则会崩溃。原因详解如下:

    PagedList.class
    @NonNull
    static <K, T> PagedList<T> create(...) {
        /**
         * PositionalDataSource时,dataSource.isContiguous()恒等于false
         * 此时config.enablePlaceholders=false才执行if逻辑
         **/
        if (dataSource.isContiguous() || !config.enablePlaceholders) {
            ...其他代码
            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);
        }
    }

create方法生成的ContiguousPagedList、TiledPagedList对象最终都会调用PositionalDataSource.dispatchLoadInitial方法,如下:
    final void dispatchLoadInitial(...) {
        /**
         * 主要生成LoadInitialCallbackImpl对象
         * ContiguousPagedList方式调用时传的acceptCount为false,TiledPagedList传的为true
         **/
        LoadInitialCallbackImpl<T> callback =
                new LoadInitialCallbackImpl<>(this, acceptCount, pageSize, receiver);
        ... 其他代码
    }

继续看LoadInitialCallbackImpl代码:
    static class LoadInitialCallbackImpl<T> extends LoadInitialCallback<T> {
        ... // 其他代码
        LoadInitialCallbackImpl(@NonNull PositionalDataSource dataSource, boolean countingEnabled, int pageSize, PageResult.Receiver<T> receiver) {
             ... // 其他代码

            // ContiguousPagedList方式调用时传的acceptCount为false,TiledPagedList传的为true
            mCountingEnabled = countingEnabled;
             ... // 其他代码
        }

        @Override
        public void onResult(@NonNull List<T> data, int position, int totalCount) {
            if (!mCallbackHelper.dispatchInvalidResultIfInvalid()) {
                ... // 其他代码

                if (mCountingEnabled) {
                    int trailingUnloadedCount = totalCount - position - data.size();
                    mCallbackHelper.dispatchResultToReceiver(
                            new PageResult<>(data, position, trailingUnloadedCount, 0));
                }
                ... // 其他代码
            }
        }

        @Override
        public void onResult(@NonNull List<T> data, int position) {
            if (!mCallbackHelper.dispatchInvalidResultIfInvalid()) {
                ... // 其他代码
                if (mCountingEnabled) {
                    throw new IllegalStateException("Placeholders requested, but totalCount not"
                            + " provided. Please call the three-parameter onResult method, or"
                            + " disable placeholders in the PagedList.Config");
                }
                ... // 其他代码
            }
        }
    }
从onResult方法代码可知mCountingEnabled为true一定会抛出异常。
所以在PagedList.create方法中需保证不创建TiledPagedList对象,即设置config.enablePlaceholder=false
2. PageKeyedDataSource:原始数据已有分页功能,根据每页的Key取得上下页数据
/**
 * desc:原始数据已有分页功能,根据每页的Key取得数据</br>
 * time: 2019/11/12-11:25</br>
 * author:Leo </br>
 */
注:其中的泛型Int是用于请求上下页数据的参数key实体类型
    其中的泛型Item是请求到列表每项数据的实体类
class TestPageKeyedDataSource : PageKeyedDataSource<Int, Item>() {
    override fun loadInitial(
        params: LoadInitialParams<Int>, callback: LoadInitialCallback<Int, Item>
    ) {
        val items = getItems(params.requestedLoadSize)
        callback.onResult(items, items[0].id, items.last().id)
    }

    // 这里的参数key实际就是上述的items.last().id
    override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, Item>) {
        params.apply {
            val items = getIncreaseItems(key, requestedLoadSize)
            callback.onResult(items, items.last().id)
        }
    }

    // 这里的参数key实际就是上述的items[0].id
    override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, Item>) {
        params.apply {
            val items = getReduceItems(key, requestedLoadSize)
            callback.onResult(items, items.first().id)
        }
    }
}

LoadInitialParams参数与PositionalDataSource一致。但构造函数只取了requestedLoadSizeplaceholdersEnabled这两个参数。调用代码如下:

    PageKeyedDataSource.java
    @Override
    final void dispatchLoadInitial(...) {
        LoadInitialCallbackImpl<Key, Value> callback =
                new LoadInitialCallbackImpl<>(this, enablePlaceholders, receiver);
        loadInitial(new LoadInitialParams<Key>(initialLoadSize, enablePlaceholders), callback);
        ...
    }

注意:该类型DataSource,参数值只取了`requestedLoadSize`和`placeholdersEnabled`这两个参数

LoadParams包含两个参数key(上页/下页的key) requestedLoadSize(需要加载的数量)
requestedLoadSize:即为config.pageSize
key:的赋值/初始化时机都在上述TestPageKeyedDataSource类的几个callback.onResult(...)方法中

参数key的赋值:

    @Override
    final void dispatchLoadAfter(...) {
        @Nullable Key key = getNextKey();
        ...
        loadAfter(new LoadParams<>(key, pageSize),...);
        ...
    }

    @Override
    final void dispatchLoadBefore(...) {
        @Nullable Key key = getPreviousKey();
        ...
        loadBefore(new LoadParams<>(key, pageSize),...);
        ...
    }
上下页key的赋值,关键代码如下:

1. void initKeys(@Nullable Key previousKey, @Nullable Key nextKey)
2. void setPreviousKey(@Nullable Key previousKey)
3. void setNextKey(@Nullable Key nextKey)

initKeys方法在LoadInitialCallback.onResult(...)中。精简代码如下
static class LoadInitialCallbackImpl<Key, Value> extends LoadInitialCallback<Key, Value> {  
    ...
    LoadInitialCallbackImpl(...) {
        ...
    }

   @Override
   public void onResult(@NonNull List<Value> data, int position, int totalCount,
                @Nullable Key previousPageKey, @Nullable Key nextPageKey) {
        ...
        // setup keys before dispatching data, so guaranteed to be ready
        mDataSource.initKeys(previousPageKey, nextPageKey);
        ...
   }

      @Override
      public void onResult(@NonNull List<Value> data, @Nullable Key previousPageKey,
                @Nullable Key nextPageKey) {
        ...
        mDataSource.initKeys(previousPageKey, nextPageKey);
        ...
   }
}

static class LoadCallbackImpl<Key, Value> extends LoadCallback<Key, Value> {
    ...
    @Override
    public void onResult(@NonNull List<Value> data, @Nullable Key adjacentPageKey) {
        ...
        if (mCallbackHelper.mResultType == PageResult.APPEND) {
            mDataSource.setNextKey(adjacentPageKey);
        } else {
            mDataSource.setPreviousKey(adjacentPageKey);
        }
        ...
    }
}

总结:初始化加载时,会设置上页/下页的关键值作为获取上页/下页数据的参数。每页需加载的数量从config.pageSize获取

  • 每页数据都返回两个参数:获取上页数据的key,获取下页数据的key。且获取每页数据时必须传入key才能请求到该页数据
  • 只要正确配置previousPageKeynextPageKey(上述代码中的items[0].id、items.last().id),上下滑动都可以加载数据,在第一页时也能滑动
3. ItemKeyedDataSource:当列表数据的Key有连续性,可根据Key找到下一页或上一页数据
/**
 * desc:当列表数据的Key有连续性,可根据Key找到下一页或上一页数据</br>
 * time: 2019/11/12-11:24</br>
 * author:Leo </br>
 */
注:泛型Int即为getKey()方法返回的类型
    泛型Item为列表各项的实体类型
class TestItemKeyedDataSource : ItemKeyedDataSource<Int, Item>() {

    // 因为每页请求都需要key,所以需配置一个初始key-requestedInitialKey 
    override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Item>) {
        params.apply {
            callback.onResult(getIncreaseItems(requestedInitialKey ?: 0, requestedLoadSize))
        }
    }

    // 这里的key是适配器中数据集最后一项数据的item.id
    override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Item>) {
        params.apply {
            callback.onResult(getIncreaseItems(key + 1, requestedLoadSize))
        }
    }

    // 这里的key是适配器中数据集第一项数据的item.id
    override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Item>) {
        params.apply {
            callback.onResult(getIncreaseItems(key, requestedLoadSize))
        }
    }

    override fun getKey(item: Item) = item.id
}

LoadInitialParams参数与上述两类型参数相同,但构造方法有点区别,精简源码如下:

    ItemKeyedDataSource.java

    @Override
    final void dispatchLoadInitial(...) {
        ...
        loadInitial(new LoadInitialParams<>(key, initialLoadSize, enablePlaceholders), callback);
        ...
    }

LoadInitialParams各参数与PagedList.Config对应关系如下:
requestedStartPosition -> LivePagedListBuilder.setInitialLoadKey()
requestedLoadSize -> config.initialLoadSizeHint
placeholdersEnabled -> config.enablePlaceholders
注:初次加载数据不需指定加载数据pageSize

LoadParams包含两个参数key(上页/下页的key) requestedLoadSize(需要加载的数量)
requestedLoadSize:即为config.pageSize
key:该key一般都为请求返回实体类中的一个字段,父类根据上页/下页拿到缓存下集合中的第一个或最后一个数据实体。然后根据子类中重写的getKey(item: T)方法指定key所对应的字段。部分代码如下:

    ItemKeyedDataSource.java

    @Override
    final void dispatchLoadAfter(...) {
        loadAfter(new LoadParams<>(getKey(currentEndItem), pageSize),...);
    }

    @Override
    final void dispatchLoadBefore(...) {
        loadBefore(new LoadParams<>(getKey(currentBeginItem), pageSize),...);
    }

getKey(currentEndItem)、getKey(currentBeginItem)。getKey为抽象方法,
子类重写指定对应字段,如下:

TestItemKeyedDataSource.java
override fun getKey(item: Item) = item.id

总结:初始化加载时,根据config配置加载当前页数据。渲染数据的同时也会缓存到Storage中。当获取上页/下页数据时,取得Storage中的第一条/最后一条数据。再通过子类重写的getKey方法,拿到请求所需实际的key。

LivePagedListBuilder、RxPagedListBuilder对比

从两个点对比:使用方式,值回调方式

  1. 如何使用
    /**
     * 最终返回的是 LiveData<PagedList<T>>类型对象
     */
    fun allTestLiveData(id: String) = this.let {
        LivePagedListBuilder(
            createFactory(id), PagedList.Config.Builder()
                .setPageSize(10)
                .setInitialLoadSizeHint(13)
                .setEnablePlaceholders(false)
                .setPrefetchDistance(3).build()
        ).build()
    }.observe(this, Observer { adapter.submitList(it) })

    /**
     * 最终返回的是 Flowable<PagedList<T>>类型对象
     */
    fun allTestFlowable(id: String) = this.let {
        RxPagedListBuilder(
            createFactory(id), PagedList.Config.Builder()
                .setPageSize(10)
                .setInitialLoadSizeHint(13)
                .setEnablePlaceholders(false)
                .setPrefetchDistance(3).build()
        ).buildFlowable(BackpressureStrategy.DROP)
    }.subscribe { adapter.submitList(it) }
  1. 值回调方式
    两种Builder一个为LiveData,一个为RxJava。
    先看第一个LivePagedListBuilder
    LivePagedListBuilder通过build方法构造了一个ComputableLiveData对象,最终会执行一个Runnable,看代码:
ComputableLiveDat.java
    @VisibleForTesting
    final Runnable mRefreshRunnable = new Runnable() {
        @WorkerThread
        @Override
        public void run() {
            boolean computed;
            do {
                ... // 其他代码
                mLiveData.postValue(value);
                ... // 其他代码
            } while (computed && mInvalid.get());
        }
    };

关键代码为postValue(value),通过LiveData的postValue方法设置数据,并在外部监听

对于RxPagedListBuilder,通过buildObservable方法创建一个Observable,将其他逻辑代码放到一个ObservableOnSubscribe类中实现,subscribe回调方法中实现PageList的构造,代码如下:

    private PagedList<Value> createPagedList() {
            ... // 其他代码
            do {
                ... // 其他代码

                mList = new PagedList.Builder<>(mDataSource, mConfig)
                        .setNotifyExecutor(mNotifyExecutor)
                        .setFetchExecutor(mFetchExecutor)
                        .setBoundaryCallback(mBoundaryCallback)
                        .setInitialKey(initializeKey)
                        .build();
            } while (mList.isDetached());
            return mList;
        }

最终结果还是为了构造一个PagedList。

两种方式最终都是为了构造一个PagedList,一个通过ComputableLiveData包裹一层通过在Runnable中调用LiveData的postValue方法通知值更新;另一个是借助RxJava的线程切换,创建后通过设置观察者取得结果。
参考文章:
https://enginebai.com/2019/04/22/android-paging-part1/

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