android paging lib

如果要看3種完整的DataSource代碼請到這里 http://www.lxweimin.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 >PagingWithNetworkSample</a>
 * shows how to implement a network PageKeyedDataSource using
 * <a >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 >PagingWithNetworkSample</a>
 * shows how to implement a network ItemKeyedDataSource using
 * <a >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) {}
    }

測試這個方法的時候又發現以前寫法有問題,所以可能導致上邊的回調不會執行

  • 1 onZeroItemsLoaded
    這個方法啥時候會回調?就是我們DataSource里的loadInitial方法里回調的result的size為0的時候,也就是第一次加載就沒數據會走這里。
    需要注意的地方,對于PositionalDataSource 這種,如果你initKey,也就是首次加載的position設置的不是0,而你又返回一個空的數據,那么會掛掉的。loadinitial允許返回size為0的數據,但你必須從0開始

  • 2 onItemAtFrontLoaded
    這個就是如果加載到了最頂部的數據的時候會回調,對于PositionalDataSource 而言,就是加載了position為0的數據的時候就會走這里了,對于ItemKeyedDataSource就是loadBefore方法里 callback.onResult(this)的this的size為0的時候,PageKeyedDataSource也一樣的道理

  • 3 onItemAtEndLoaded
    加載到沒有數據的時候會返回最后一條數據,也就是在loadRange里的callback.onResult(this),這個this集合的大小為0的時候,所以啊如果要監聽到onItemAtEndLoaded這個回調,你的callback.onResult必須調用,返回一個size為0的集合。而不是我們上邊demo的代碼,直接不調用這個方法

所以如果要監聽上邊這個回調,那么在返回數據的時候就得處理下,如果沒有數據了,就返回一個size為0的集合,而不是不調用callback.onResult(list)方法

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,886評論 18 139
  • 1. Java基礎部分 基礎部分的順序:基本語法,類相關的語法,內部類的語法,繼承相關的語法,異常的語法,線程的語...
    子非魚_t_閱讀 31,759評論 18 399
  • 1.ios高性能編程 (1).內層 最小的內層平均值和峰值(2).耗電量 高效的算法和數據結構(3).初始化時...
    歐辰_OSR閱讀 29,566評論 8 265
  • Spring Boot 參考指南 介紹 轉載自:https://www.gitbook.com/book/qbgb...
    毛宇鵬閱讀 46,941評論 6 342
  • 我做鬼臉扮可愛(比你可愛) 你做鬼臉很耍帥(比我還帥) 我要比你養的貓咪叫小白還可愛 你說的段子不好笑輸給我了卻耍...
    徐秀美閱讀 147評論 0 0