android paging lib

2018-06-13  本文已影响149人  有点健忘

如果要看3种完整的DataSource代码请到这里 https://www.jianshu.com/p/fd00c0fbd774

参考文章

https://juejin.im/entry/5a6fd4b7f265da3e261c3cc4
https://developer.android.com/reference/android/arch/paging/DataSource
http://www.loongwind.com/archives/367.html
之前用的PagedList.Builder生成的PagedList,然后用loadAround刷新,不太好使啊,改天研究为啥
现在改用LivePagedListBuilder生成LiveData然后添加监听

 data.observe(this, Observer {
            println("98==================observer====${it?.size}")
            getAdapter().submitList(it)
        })

room还没学
https://blog.csdn.net/youth_never_go_away/article/details/79902879

Datasource

顾名思义, Datasource<Key,Value>是数据源相关的类,其中 Key对应加载数据的条件信息, Value对应返回结果, 针对不同场景,Paging 提供了三种 Datasource:

PageKeyedDataSource <Key , Value> :适用于目标数据根据页信息请求数据的场景,即 Key 字段是页相关的信息。比如请求的数据的参数中包含类似 next /previous的信息。

ItemKeyedDataSource <Key , Value> :适用于目标数据的加载依赖特定item的信息, 即Key字段包含的是Item中的信息,比如需要根据第N项的信息加载第N+1项的数据,传参中需要传入第N项的ID时,该场景多出现于论坛类应用评论信息的请求。

PositionalDataSource <T > :适用于目标数据总数固定,通过特定的位置加载数据,这里 Key是Integer类型的位置信息, T即 Value。 比如从数据库中的1200条开始加在20条数据。

以上三种 Datasource 都是抽象类, 使用时需实现请加载数据的方法。三种Datasource 都需要实现 loadInitial()方法, 各自都封装了请求初始化数据的参数类型 LoadInitialParams。 不同的是分页加载数据的方法, PageKeyedDataSource和 ItemKeyedDataSource比较相似, 需要实现 loadBefore()和 loadAfter () 方法,同样对请求参数做了封装,即 LoadParams<Key>。 PositionalDataSource需要实现 loadRange () ,参数的封装类为 LoadRangeParams。

如果项目中使用 Android 架构组件中的 Room, Room 可以创建一个产出 PositionalDataSource的 DataSource .Factory:

先贴下代码

数据有问题,从0到29完事就都成了16到29拉,刚学习写还不太懂逻辑
现在对代码修改过,可以正常加载了

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_paging)
        defaultSetTitle("page")

        //recyclerview 设置adapter以及分割线
        rv_paging.apply {
            layoutManager = LinearLayoutManager(this@ActivityPaging)
            //弄条分割线
            addItemDecoration(object : RecyclerView.ItemDecoration() {
                var paint = Paint()
                override fun getItemOffsets(outRect: Rect, view: View?, parent: RecyclerView?, state: RecyclerView.State?) {
                    super.getItemOffsets(outRect, view, parent, state)
                    outRect.bottom = 3
                }

                override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State?) {
                    super.onDraw(c, parent, state)
                    paint.color = Color.LTGRAY
                    var childCount = parent.childCount
                    repeat(childCount) {
                        var child = parent.getChildAt(it)
                        if (child != null) {
                            c.drawRect(parent.paddingLeft.toFloat(), child.bottom.toFloat(), parent.width.toFloat() - parent.paddingRight, child.bottom + 3f, paint)
                        }
                    }
                }
            })
            adapter = MyPagingAdapter(callback)
        }
        //创建pageList
        makePageList()

    }

    private fun getAdapter(): MyPagingAdapter {
        return rv_paging.adapter as MyPagingAdapter
    }

    private fun makePageList() {
        val mPagedListConfig = PagedList.Config.Builder()
                .setPageSize(10) //分页数据的数量。在后面的DataSource之loadRange中,count即为每次加载的这个设定值。
                .setPrefetchDistance(10) //提前多少数据开始继续加载数据。如果是上滑的话对应的就是离最后个item还有2个位置的时候开始记载更多数据。下拉一样的道理
                .setInitialLoadSizeHint(10)
                .setEnablePlaceholders(false)
                .build()

        //不建议这种,因为还得自己处理Executor,建议使用下边注释的代码
        var mPagedList = PagedList.Builder(MyDataSource(), mPagedListConfig)
                .setNotifyExecutor {
                    println("setNotifyExecutor=============1=====${Thread.currentThread().name}")//进来是非主线程pool-7-thread-1,因为这个是更新ui的,所以现编切换到主线程
                    Handler(Looper.getMainLooper()).post(it)//弄个全局变量,不要每次都new一个,我这里就方便测试
                }
                .setFetchExecutor {
                    println("setFetchExecutor=========1=========${Thread.currentThread().name}") //这里进来的是main线程,因为你要加载数据,所以切换线程
                    Executors.newFixedThreadPool(2).execute(it) //这里不应该每次都new一个,因改写个全局变量
                }
                .setInitialKey(initKey)//不设置的话默认就是0
                .build()
        getAdapter().submitList(mPagedList)

        //如果懒得自己处理setNotifyExecutor和setFetchExecutor,建议用下边的,系统都有默认值,省事
//         LivePagedListBuilder(object : DataSource.Factory<Int, Student>() {
//            override fun create(): DataSource<Int, Student> {
//                return MyDataSource() //DataSource有3种,这里就简单随便写了个,自己看需求写
//            }
//        }, mPagedListConfig)
//                .build()
//                .observe(this, Observer {
//                    getAdapter().submitList(it)
//                })
    }
    var initKey=20;//初始从哪个位置开始加载数据,这里测试从第20条开始,这样下拉可以看到前20条数据,上拉可以看到20之后的数据
    inner class MyDataSource : PositionalDataSource<Student>() {

        private fun loadRangeInternal(startPosition: Int, loadCount: Int): List<Student>? {
            // actual load code here
            if (startPosition > 70) { //模拟数据加载完的情况
                return null
            }
            var list = arrayListOf<Student>()
            repeat(loadCount) {
                list.add(Student(startPosition + it + 1, "stu ${startPosition + it + 1}"))
            }
            return list
        }

        override fun loadInitial(@NonNull params: PositionalDataSource.LoadInitialParams,
                                 @NonNull callback: PositionalDataSource.LoadInitialCallback<Student>) {
            //加载数据这里可以自己根据实际需求来处理params.requestedStartPosition就是我们设置的initKey=20
            loadRangeInternal(params.requestedStartPosition, params.requestedLoadSize)?.apply {
                callback.onResult(this, params.requestedStartPosition)
            }

        }

        override fun loadRange(@NonNull params: PositionalDataSource.LoadRangeParams,
                               @NonNull callback: PositionalDataSource.LoadRangeCallback<Student>) {
            //加载数据这里可以自己根据实际需求来处理
            println("132=====load range  ${params.startPosition}==${params.loadSize}===${Thread.currentThread().name}")
            loadRangeInternal(params.startPosition, params.loadSize)?.apply {
                callback.onResult(this)
            }

        }

    }

    //adapter和我们以前的没太大区别,就是构造方法里需要传一个参数,用来判断是否是相同数据而已
    open inner class MyPagingAdapter : PagedListAdapter<Student, BaseRvHolder> {
        constructor(diffCallback: DiffUtil.ItemCallback<Student>) : super(diffCallback)

        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseRvHolder {

            return BaseRvHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_paging, parent, false))
        }

        override fun onBindViewHolder(holder: BaseRvHolder, position: Int) {
            getItem(position)?.apply {
                holder.setText(R.id.tv_name, name)
                holder.setText(R.id.tv_age, "${age}")
            }
            println("onBindViewHolder=============$position//${itemCount} ===${getItem(position)}")
        }

    }

    val callback = object : DiffUtil.ItemCallback<Student>() {
        override fun areItemsTheSame(oldItem: Student?, newItem: Student?): Boolean {
            return oldItem?.id == newItem?.id
        }

        override fun areContentsTheSame(oldItem: Student?, newItem: Student?): Boolean {
            return oldItem?.age == newItem?.age
        }
    }

看看源码好分析

看日志知道,首先执行了一次DataSource里的loadInitial的方法。
所以查找下这个方法啥时候执行的
首先会蹦到PositionalDataSource 的代码里。【因为我上边用的就是这个DataSource,如果是其他2种datasource,就会跳到对应的类里了】
然后一层一层往上,最后会发现走到了ContiguousPagedList的构造方法里
看下2张图


image.png image.png

而这两个构造方法都是在PagedList创建的时候执行的,如下图
所以我们就知道,在PagedList初始化的时候,DataSource里的loadInitial方法就会执行一次。


image.png

看上图的if条件,我们知道datasource的isContiguous为true或者config的enablePlaceholers为false的时候是ContigousPageList,其他的是TiledPageList
我们知道datasource有3种,其中有2个都是继承一个父类的,pagekeydDatasource和ItemKeyedDatasrouce都是继承ContigousDatasource【看名字就知道这2个isContiguous为true了】,另外一个positionalDataSource就是false拉。

分析为啥pageSize我设置为10,结果loadInitial里默认加载就是30条,一就是3倍?

一路点击这个LoadInitialParams 哪里来的,最后发现它就是PagedList里Config类里配置的。
看下Config的Build类

 public static final class Builder {
            private int mPageSize = -1;
            private int mPrefetchDistance = -1;
            private int mInitialLoadSizeHint = -1;
            private boolean mEnablePlaceholders = true;



            public Config build() {
                if (mPageSize < 1) {
                    throw new IllegalArgumentException("Page size must be a positive number");
                }
                if (mPrefetchDistance < 0) {
                    mPrefetchDistance = mPageSize;
                }
                if (mInitialLoadSizeHint < 0) {
                    mInitialLoadSizeHint = mPageSize * 3;//因为我们的config啥都没配置,就设置了一个pagesize
                }
                if (!mEnablePlaceholders && mPrefetchDistance == 0) {
//这里也要注意,mEnablePlaceholders =false的时候,预加载的个数不能为0
                    throw new IllegalArgumentException("Placeholders and prefetch are the only ways"
                            + " to trigger loading of more data in the PagedList, so either"
                            + " placeholders must be enabled, or prefetch distance must be > 0.");
                }

                return new Config(mPageSize, mPrefetchDistance,
                        mEnablePlaceholders, mInitialLoadSizeHint);
            }

看下PagedList的Builder构造方法,就是我们demo里用的这个,其实也就是传了一个Config
,修改后的demo传了个config,最早用的就是下边的,就传了个pagesize

        public Builder(@NonNull DataSource<Key, Value> dataSource, int pageSize) {
            this(dataSource, new PagedList.Config.Builder().setPageSize(pageSize).build());
        }

修改config的时候又挂了。

结论:setInitialLoadSizeHint的大小必须是pagesize的倍数,也就是必须可以整除

        val mPagedListConfig = PagedList.Config.Builder()
                .setPageSize(5) //分页数据的数量。在后面的DataSource之loadRange中,count即为每次加载的这个设定值。
                .setPrefetchDistance(5) //初始化时候,预取数据数量。
                .setInitialLoadSizeHint(6)//这玩意不能瞎写,这里写个6就挂了,下边分析原因
                .setEnablePlaceholders(false)
                .build()

        override fun loadInitial(@NonNull params: PositionalDataSource.LoadInitialParams,
                        @NonNull callback: PositionalDataSource.LoadInitialCallback<Student>) {

            val totalCount = computeCount()
            val position = PositionalDataSource.computeInitialLoadPosition(params, totalCount)
            val loadSize = PositionalDataSource.computeInitialLoadSize(params, position, totalCount)
           //然后下边就挂了。我初始加载了6条数据
            callback.onResult(loadRangeInternal(position, loadSize), position, totalCount)
        }

走到了下边这里,然后分析下


image.png

可以看到,我们初始的时候data大小是6,完事pagesize是5,这个求余肯定不是0了。

上边datasource有3种,而这里pagedlist有2种,根据datasource来的

if (dataSource.isContiguous() || !config.enablePlaceholders){
ContiguousPagedList
}else{
 return new TiledPagedList<>
}

继续看PageKeyedDataSource和ItemKeyedDataSource都是contiguous的

/**
 * Incremental data loader for page-keyed content, where requests return keys for next/previous
 * pages.
 * <p>
 * Implement a DataSource using PageKeyedDataSource if you need to use data from page {@code N - 1}
 * to load page {@code N}. This is common, for example, in network APIs that include a next/previous
 * link or key with each page load.
 * <p>
 * The {@code InMemoryByPageRepository} in the
 * <a href="https://github.com/googlesamples/android-architecture-components/blob/master/PagingWithNetworkSample/README.md">PagingWithNetworkSample</a>
 * shows how to implement a network PageKeyedDataSource using
 * <a href="https://square.github.io/retrofit/">Retrofit</a>, while
 * handling swipe-to-refresh, network errors, and retry.
 *
 * @param <Key> Type of data used to query Value types out of the DataSource.
 * @param <Value> Type of items being loaded by the DataSource.
 */
public abstract class PageKeyedDataSource<Key, Value> extends ContiguousDataSource<Key, Value> 




/**
 * Incremental data loader for paging keyed content, where loaded content uses previously loaded
 * items as input to future loads.
 * <p>
 * Implement a DataSource using ItemKeyedDataSource if you need to use data from item {@code N - 1}
 * to load item {@code N}. This is common, for example, in sorted database queries where
 * attributes of the item such just before the next query define how to execute it.
 * <p>
 * The {@code InMemoryByItemRepository} in the
 * <a href="https://github.com/googlesamples/android-architecture-components/blob/master/PagingWithNetworkSample/README.md">PagingWithNetworkSample</a>
 * shows how to implement a network ItemKeyedDataSource using
 * <a href="https://square.github.io/retrofit/">Retrofit</a>, while
 * handling swipe-to-refresh, network errors, and retry.
 *
 * @param <Key> Type of data used to query Value types out of the DataSource.
 * @param <Value> Type of items being loaded by the DataSource.
 */
public abstract class ItemKeyedDataSource<Key, Value> extends ContiguousDataSource<Key, Value> 

到底怎么才能自动加载数据

目前是使用如下的代码解决的,也就是不是直接build一个pagelist,而是用如下的代码生成一个包含pagelist的livedata,然后添加observer

        LivePagedListBuilder(MyDataSourceFactory(), getPageConfig()).build().observe(this, Observer {
            println("base 34==================observer====${it?.size}")
            getAdapter().submitList(it)
        })

这两天我就仔细的看了一遍又一遍LivePagedListBuilder构建的PageList和我直接build的到底有啥区别。真是没看出来啊。
首先看下原来有问题的,在实际中,我们可以看到setFetchExecutor 里的方法有执行过,看名字也像是获取数据用的,一直没当回事

PagedList.Builder(MyDataSource(), mPagedListConfig)
                            .setNotifyExecutor {
                                println("setNotifyExecutor=============1=====${Thread.currentThread().name}")
                            }
                            .setFetchExecutor {
                                println("setFetchExecutor=========1=========${Thread.currentThread().name}")
                            }
                            .build()

今天心血来潮,我就把那个正常的也加了这个,如下,然后我就发现它没数据了。

 LivePagedListBuilder(object :DataSource.Factory<Int,Student>(){
            override fun create(): DataSource<Int, Student> {
                return MyDataSource()
            }
        }, mPagedListConfig)
             .setFetchExecutor {
                 println("setFetchExecutor=========1=========${Thread.currentThread().name}")
             }
             .build()

setFetchExecutor 有个默认值,那就来看下默认值都干啥了,为啥用默认值可以加载出数据?

public final class LivePagedListBuilder<Key, Value> {
 
    private Executor mFetchExecutor = ArchTaskExecutor.getIOThreadExecutor();

长这样,额,原来默认的里边是有执行这个runnable了,而我们写的就打印了下,根本没操作这个runnable。

    private static final Executor sIOThreadExecutor = new Executor() {
        @Override
        public void execute(Runnable command) {
            getInstance().executeOnDiskIO(command);
        }
    };

终于解决了为啥不能加载更多的数据,代码如下

setNotifyExecutor 和setFetchExecutor 是关键啊,这两个里边返回的runnable是需要我们来执行了。还以为这个就是看的。

        var mPagedList = PagedList.Builder(MyDataSource(), mPagedListConfig)
                .setNotifyExecutor {
                    println("setNotifyExecutor=============1=====${Thread.currentThread().name}")//进来是非主线程pool-7-thread-1,因为这个是更新ui的,所以下边切换到主线程
                    Handler(Looper.getMainLooper()).post(it)//弄个全局变量,不要每次都new一个,我这里就方便测试
                }
                .setFetchExecutor {
                    println("setFetchExecutor=========1=========${Thread.currentThread().name}") //这里进来的是main线程,因为你要加载数据,所以切换线程
                    Executors.newFixedThreadPool(2).execute(it) //这里不应该每次都new一个,因改写个全局变量
                }
                .build()
        getAdapter().submitList(mPagedList)

简单分析下是如何实现自动加载数据的

这就要看新的adapter了

//PagedListAdapter里获取数据的方法如下
      protected T getItem(int position) {
        return mDiffer.getItem(position);
    }
//继续进入mDiffer里查看
    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);
    }
  
    /**
     * Load adjacent items to passed index.
     *
     * @param index Index at which to load.
     */
    public void loadAround(int index) {
        mLastLoad = index + getPositionOffset();
        loadAroundInternal(index);

        mLowestIndexAccessed = Math.min(mLowestIndexAccessed, index);
        mHighestIndexAccessed = Math.max(mHighestIndexAccessed, index);

        /*
         * mLowestIndexAccessed / mHighestIndexAccessed have been updated, so check if we need to
         * dispatch boundary callbacks. Boundary callbacks are deferred until last items are loaded,
         * and accesses happen near the boundaries.
         *
         * Note: we post here, since RecyclerView may want to add items in response, and this
         * call occurs in PagedListAdapter bind.
         */
        tryDispatchBoundaryCallbacks(true);
    }

其中loadAroundInternal(index);
这个就是在根据当前加载是数据,判断是否可以加载前边的数据,以及后边的数据,就比如我们demo里,我初始是从position为20开始加载数据的,pagesize为10的话,这里就会预加载前10条数据,以后后10条数据,也就是10到19,以及30到39
至于tryDispatchBoundaryCallbacks这个,就是处理数据加载边界值一些回调,因为我们的pagelist也没设置callback,所以不研究了。
大家可以看下这个回调的方法都有啥,有需要可以自己加


    public abstract static class BoundaryCallback<T> {
        /**
         * Called when zero items are returned from an initial load of the PagedList's data source.
         */
        public void onZeroItemsLoaded() {}

        /**
         * Called when the item at the front of the PagedList has been loaded, and access has
         * occurred within {@link Config#prefetchDistance} of it.
         * <p>
         * No more data will be prepended to the PagedList before this item.
         *
         * @param itemAtFront The first item of PagedList
         */
        public void onItemAtFrontLoaded(@NonNull T itemAtFront) {}

        /**
         * Called when the item at the end of the PagedList has been loaded, and access has
         * occurred within {@link Config#prefetchDistance} of it.
         * <p>
         * No more data will be appended to the PagedList after this item.
         *
         * @param itemAtEnd The first item of PagedList
         */
        public void onItemAtEndLoaded(@NonNull T itemAtEnd) {}
    }

测试这个方法的时候又发现以前写法有问题,所以可能导致上边的回调不会执行

所以如果要监听上边这个回调,那么在返回数据的时候就得处理下,如果没有数据了,就返回一个size为0的集合,而不是不调用callback.onResult(list)方法

上一篇下一篇

猜你喜欢

热点阅读