如果要看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張圖
而這兩個構造方法都是在PagedList創建的時候執行的,如下圖
所以我們就知道,在PagedList初始化的時候,DataSource里的loadInitial方法就會執行一次。
看上圖的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)
}
走到了下邊這里,然后分析下
可以看到,我們初始的時候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)方法