本文總結了JetPack中Paging3的相關用法和示例
一、Paging是什麼想想我們之前的業務中,實現分頁載入需要怎麼處理?
一般我們都是自己封裝RecycleView或者使用XRecycleView這種第三方庫去做,而Paging 就是Google為我們提供的分頁功能的標準庫,這樣我們就無須自己去基於RecycleView實現分頁功能,並且Paging為我們提供了許多可配置選項,使得分頁功能更加靈活。
而Paging3是Paging庫當前的最新版本,仍處於測試版本,相比較於Paging2的使用就簡潔多了。
二、Paging的使用專案搭建
首先我們新建專案,在gradle中引用paging庫如下:
def paging_version = "3.0.0-alpha07"implementation "androidx.paging:paging-runtime:$paging_version"testImplementation "androidx.paging:paging-common:$paging_version"
專案示例,我們使用Kotlin語言並且使用了協程和Flow,所以也需要新增協程的庫如下:
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.7-mpp-dev-11'
專案示例
在官方文件中也給出了我們Paging在架構中的使用圖。
透過上圖我們也可以清晰的看出來,Paging在倉庫層、ViewModel和UI層都有具體的表現,接下來我們透過一個示例來逐步講解Paging是如何在專案架構中工作的。
API介面準備
這裡我們已經寫好了RetrofitService類用於建立網路請求的service程式碼如下所示:
和 DataApi介面,這裡我們將方法宣告為掛起函式,便於在協程中呼叫。
interface DataApi { /** * 獲取資料 */ @GET("wenda/list/{pageId}/json") suspend fun getData(@Path("pageId") pageId:Int): DemoReqData}
定義資料來源
首先我們來定義資料來源DataSource繼承自PagingSource,程式碼如下所示:
class DataSource():PagingSource<Int,DemoReqData.DataBean.DatasBean>(){ override suspend fun load(params: LoadParams<Int>): LoadResult<Int, DemoReqData.DataBean.DatasBean> { TODO("Not yet implemented") }}
我們可以看到PagingSource中有兩個引數Key 和 Value,這裡Key我們定義為Int型別Value DemoReqData 是介面返回資料對應的實體類,這裡的意思就是我們傳Int型別的值(如頁碼)得到返回的資料資訊DemoReqData物件。
這裡需要提醒的是如果你使用的不是Kotlin 協程而是Java,則需要繼承對應的PagingSource如RxPagingSource或ListenableFuturePagingSource。
DataSource為我們自動生成了load方法,我們主要的請求操作就在load方法中完成。主要程式碼如下所示:
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, DemoReqData.DataBean.DatasBean> { return try { //頁碼未定義置為1 var currentPage = params.key ?: 1 //倉庫層請求資料 var demoReqData = DataRespority().loadData(currentPage) //當前頁碼 小於 總頁碼 頁面加1 var nextPage = if (currentPage < demoReqData?.data?.pageCount ?: 0) { currentPage + 1 } else { //沒有更多資料 null } if (demoReqData != null) { LoadResult.Page( data = demoReqData.data.datas, prevKey = null, nextKey = nextPage ) } else { LoadResult.Error(throwable = Throwable()) } } catch (e: Exception) { LoadResult.Error(throwable = e) }}
上面程式碼我們可以看到在datasource中我們透過DataRespority()倉庫層,去請求資料,如果沒有更多資料就返回null,最後使用 LoadResult.Page將結果返回,如果載入失敗則用LoadResult.Error返回,由於 LoadResult.Page中的data 必須是非空型別的,所以我們需要判斷返回是否為null。
接下來我們看下DataRespority倉庫層的程式碼,程式碼比較簡單,如下所示:
class DataRespority { private var netWork = RetrofitService.createService( DataApi::class.java ) /** * 查詢護理資料 */ suspend fun loadData( pageId: Int ): DemoReqData? { return try { netWork.getData(pageId) } catch (e: Exception) { //在這裡處理或捕獲異常 null } }}
Load呼叫官方給出的流程如下所示:
從上圖可以知道,load的方法 是我們透過Paging的配置自動觸發的,不需要我們每次去呼叫,那麼我們如何來使用DataSource呢?
呼叫PagingSource
The Pager object calls the load() method from the PagingSource object, providing it with the LoadParams object and receiving the LoadResult object in return.
這句話翻譯過來的意思就是:Pager物件從PagingSource物件呼叫load()方法,為它提供LoadParams物件,並作為回報接收LoadResult物件。
所以我們在建立viewModel物件,並建立pager物件從而呼叫PagingSource方法 ,程式碼如下所示:
class MainActivityViewModel : ViewModel() { /** * 獲取資料 */ fun getData() = Pager(PagingConfig(pageSize = 1)) { DataSource() }.flow}
在viewmodel中我們定義了一個getData的方法,Pager中透過配置PagingConfig來實現特殊的定製,我們來看下PagingConfig中的引數如下:
pageSize:定義從PagingSource一次載入的專案數。
prefetchDistance:預取距離,簡單解釋就是 當距離底部還有多遠的時候自動載入下一頁,即自動呼叫load方法,預設值和pageSize相等。
enablePlaceholders:是否顯示佔位符,當網路不好的時候,可以考到頁面的框架,從而提升使用者體驗。
還有一些其他引數這裡就不一一介紹了,從構造方法的原始碼中可以看出pageSize這個引數是必填的,其他的是可選項,所以我們這裡傳了1。
定義RecycleViewAdapter
這一步,和我們平時定義普通的RecycleViewAdapter沒有太大的區別,只是我們繼承的是PagingDataAdapter,主要程式碼如下所示:
class DataRecycleViewAdapter : PagingDataAdapter<DemoReqData.DataBean.DatasBean, RecyclerView.ViewHolder>(object : DiffUtil.ItemCallback<DemoReqData.DataBean.DatasBean>() { override fun areItemsTheSame( oldItem: DemoReqData.DataBean.DatasBean, newItem: DemoReqData.DataBean.DatasBean ): Boolean { return oldItem.id == newItem.id } @SuppressLint("DiffUtilEquals") override fun areContentsTheSame( oldItem: DemoReqData.DataBean.DatasBean, newItem: DemoReqData.DataBean.DatasBean ): Boolean { return oldItem == newItem } }) { override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { var dataBean = getItem(position) (holder as DataViewHolder).binding.demoReaData = dataBean } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TestViewHolder { return TestViewHolder( DataBindingUtil.inflate( LayoutInflater.from(parent.context), R.layout.health_item_test, parent, false ) )} inner class DataViewHolder(private val dataBindingUtil: ItemDataBinding) : RecyclerView.ViewHolder(dataBindingUtil.root) { var binding = dataBindingUtil }}
這裡我們要提醒的是DiffUtil這個引數,用於計算列表中兩個非空專案之間的差異的回撥。無特殊情況一般都是固定寫法。
View層資料請求並將結果顯示在View上
到這裡,基本工作已經差不多了,當然我們說的差不多了只是快能看到成果了,其中需要講解的地方還有很多,最後一步我們在view中請求資料,並將結果繫結在adapter上。我們在View程式碼中呼叫viewModel中的getData方法,程式碼如下所示:
val manager = LinearLayoutManager(this)rv_data.layoutManager = managerrv_data.adapter = dataRecycleViewAdapterbtn_get.setOnClickListener { lifecycleScope.launch { mainActivityViewModel.getData().collectLatest { dataRecycleViewAdapter.submitData(it) } }}
我們在協程中呼叫getData方法,接收最新的資料,透過PagingAdapter的submitData方法為adapter提供資料,執行結果如下所示(忽略醜陋的UI.jpg)。
當我們往下滑動時,當底部還剩1個(pageSize)資料的時候會自動載入下一頁。
當然對於這個介面不需要傳pageSize,所以返回的資料大小並不會受pageSize的影響,如此一來,我們就使用Paging3 完成了簡單的資料分頁請求。
三、Paging的載入狀態Paging3 為我們提供了獲取Paging載入狀態的方法,其中包含新增監聽事件的方式以及在adapter中直接顯示的方式,首先我們來看監聽事件的方式。
使用監聽事件方式獲取載入狀態
上面我們在Activity中建立了dataRecycleViewAdapter來顯示頁面資料,我們可以使用addLoadStateListener方法新增載入狀態的監聽事件,如下所示:
dataRecycleViewAdapter.addLoadStateListener { when (it.refresh) { is LoadState.NotLoading -> { Log.d(TAG, "is NotLoading") } is LoadState.Loading -> { Log.d(TAG, "is Loading") } is LoadState.Error -> { Log.d(TAG, "is Error") } }}
這裡的it是CombinedLoadStates資料類,有refresh、Append、Prepend 區別如下表格所示:
也就是說如果監測的是it.refresh,當載入第二頁第三頁的時候,狀態是監聽不到的,這裡只以it.refresh為例。
LoadState的值有三種,分別是NotLoading:當沒有載入動作並且沒有錯誤的時候。Loading和Error顧名思義即對應為正在載入 和載入錯誤的時候,監聽方式除了addLoadStateListener外,還可以直接使用loadStateFlow的方式,由於flow內部是一個掛起函式 所以我們需要在協程中執行,程式碼如下所示:
lifecycleScope.launch { dataRecycleViewAdapter.loadStateFlow.collectLatest { when (it.refresh) { is LoadState.NotLoading -> { } is LoadState.Loading -> { } is LoadState.Error -> { } } }}
2020-11-14 16:39:19.841 23729-23729/com.example.pagingdatademo D/MainActivity: is NotLoading2020-11-14 16:39:24.529 23729-23729/com.example.pagingdatademo D/MainActivity: 點選了查詢按鈕2020-11-14 16:39:24.651 23729-23729/com.example.pagingdatademo D/MainActivity: is Loading2020-11-14 16:39:25.292 23729-23729/com.example.pagingdatademo D/MainActivity: is NotLoading
首先是NotLoading 狀態,因為我們什麼都沒有操作,點選了查詢按鈕後變成Loading狀態因為正在載入資料,查詢結束後再次回到了NotLoading的狀態,符合我們的預期,那這個狀態有什麼用呢? 我們在Loading狀態顯示一個progressBar過渡提升使用者體驗等,當然最重要的還是Error狀態,因為我們需要Error狀態下告知使用者。
2020-11-14 16:48:25.943 26846-26846/com.example.pagingdatademo D/MainActivity: is NotLoading2020-11-14 16:48:27.218 26846-26846/com.example.pagingdatademo D/MainActivity: 點選了查詢按鈕2020-11-14 16:48:27.315 26846-26846/com.example.pagingdatademo D/MainActivity: is Loading2020-11-14 16:48:27.322 26846-26846/com.example.pagingdatademo D/MainActivity: is Error
這裡要注意的是什麼呢,就是這個Error的狀態,不是Paging為我們自動返回的,而是我們在DataSource中捕獲異常後,使用LoadResult.Error方法告知的。
我們也需要在Error狀態下監聽具體的錯誤,無網路的話就顯示無網路UI 伺服器異常的話 就提示伺服器異常,程式碼如下所示:
is LoadState.Error -> { Log.d(TAG, "is Error:") when ((it.refresh as LoadState.Error).error) { is IOException -> { Log.d(TAG, "IOException") } else -> { Log.d(TAG, "others exception") } }}
我們在斷網狀態下,點選查詢,日誌如下所示:
2020-11-14 17:29:46.234 12512-12512/com.example.pagingdatademo D/MainActivity: 點選了查詢按鈕2020-11-14 17:29:46.264 12512-12512/com.example.pagingdatademo D/MainActivity: 請求第1頁2020-11-14 17:29:46.330 12512-12512/com.example.pagingdatademo D/MainActivity: is Loading2020-11-14 17:29:46.339 12512-12512/com.example.pagingdatademo D/MainActivity: is Error:2020-11-14 17:29:46.339 12512-12512/com.example.pagingdatademo D/MainActivity: IOException
在adapter中顯示
Paging3為我們提供了新增底部、頭部adapter的方法,分別為withLoadStateFooter、withLoadStateHeader以及同時新增頭部和尾部方法withLoadStateHeaderAndFooter,這裡我們以新增尾部方法為例。
<layout> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent"> <LinearLayout android:id="@+id/ll_loading" android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center" android:orientation="horizontal" android:visibility="gone" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="正在載入資料... ..." android:textSize="18sp" /> <ProgressBar android:layout_width="20dp" android:layout_height="20dp" /> </LinearLayout> <Button android:id="@+id/btn_retry" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="載入失敗,重新請求" android:visibility="gone" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/ll_loading" /> </androidx.constraintlayout.widget.ConstraintLayout></layout>
正在載入提示和重新請求的佈局預設都是隱藏,LoadStateViewHolder程式碼如下所示:
class LoadStateViewHolder(parent: ViewGroup, var retry: () -> Void) : RecyclerView.ViewHolder( LayoutInflater.from(parent.context) .inflate(R.layout.item_loadstate, parent, false)) { var itemLoadStateBindingUtil: ItemLoadstateBinding = ItemLoadstateBinding.bind(itemView) fun bindState(loadState: LoadState) { if (loadState is LoadState.Error) { itemLoadStateBindingUtil.btnRetry.visibility = View.VISIBLE itemLoadStateBindingUtil.btnRetry.setOnClickListener { retry() } } else if (loadState is LoadState.Loading) { itemLoadStateBindingUtil.llLoading.visibility = View.VISIBLE } }}
bindState即為設定資料,根據State的狀態來顯示不同的UI。
接著我們來建立LoadStateFooterAdapter 繼承自LoadStateAdapter,對應的viewHolder即為LoadStateViewHolder,程式碼如下所示:
class LoadStateFooterAdapter(private val retry: () -> Void) : LoadStateAdapter<LoadStateViewHolder>() { override fun onBindViewHolder(holder: LoadStateViewHolder, loadState: LoadState) { (holder as LoadStateViewHolder).bindState(loadState) } override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): LoadStateViewHolder { return LoadStateViewHolder(parent, retry) }}
這裡的程式碼比較簡單,就不作講解了,最後我們來新增這個adapter.
rv_data.adapter = dataRecycleViewAdapter.withLoadStateFooter(footer = LoadStateFooterAdapter(retry = { dataRecycleViewAdapter.retry() }))
這裡要注意的是,應該把withLoadStateFooter返回的adapter設定給recyclerview,如果你是這樣寫:dataRecycleViewAdapter.withLoadStateFooter後 在單獨設定recycleView的adapter,則會是沒有效果的。
如此,我們就在adapter中完成了資料載入狀態的顯示。
除此之外,Paging3中還有一個比較重要的RemoteMediator,用來更好的載入網路資料庫和本地資料庫,我們後續有機會再為大家單獨分享吧~
四、2020年11月21日更新五、對單個item的修改修改DataRecycleViewAdapter程式碼如下所示:
class DataRecycleViewAdapter( val itemUpdate: (Int, DemoReqData.DataBean.DatasBean?,DataRecycleViewAdapter) -> Unit) : PagingDataAdapter<DemoReqData.DataBean.DatasBean, RecyclerView.ViewHolder>(object : DiffUtil.ItemCallback<DemoReqData.DataBean.DatasBean>() { override fun areItemsTheSame( oldItem: DemoReqData.DataBean.DatasBean, newItem: DemoReqData.DataBean.DatasBean ): Boolean { return oldItem.id == newItem.id } @SuppressLint("DiffUtilEquals") override fun areContentsTheSame( oldItem: DemoReqData.DataBean.DatasBean, newItem: DemoReqData.DataBean.DatasBean ): Boolean { return oldItem == newItem } }) { override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { val dataBean = getItem(position) (holder as DataViewHolder).binding.demoReaData = dataBean holder.binding.btnUpdate.setOnClickListener { itemUpdate(position, dataBean,this) } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { val binding: ItemDataBinding = DataBindingUtil.inflate( LayoutInflater.from(parent.context), R.layout.item_data, parent, false ) return DataViewHolder(binding) } inner class DataViewHolder(private val dataBindingUtil: ItemDataBinding) : RecyclerView.ViewHolder(dataBindingUtil.root) { var binding = dataBindingUtil }}
private var dataRecycleViewAdapter = DataRecycleViewAdapter { position, it, adapter -> it?.author = "黃林晴${position}" adapter.notifyDataSetChanged()}
A PagingSource / PagingData pair is a snapshot of the data set. A new PagingData / PagingData must be created if an update occurs, such as a reorder, insert, delete, or content update occurs. A PagingSource must detect that it cannot continue loading its snapshot (for instance, when Database query notices a table being invalidated), and call invalidate. Then a new PagingSource / PagingData pair would be created to represent data from the new state of the database query.
七、最後為了幫助大家深刻理解Android相關知識點的原理以及面試相關知識,在這裡我也分享一份乾貨。
由大佬收錄整理的Android學習PDF+架構影片+原始碼筆記,還有高階架構技術進階腦圖、Android開發面試專題資料,高階進階架構資料幫助大家學習提升進階,也節省大家在網上搜索資料的時間來學習,也可以分享給身邊好友一起學習。