首頁>技術>

本文總結了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開發面試專題資料,高階進階架構資料幫助大家學習提升進階,也節省大家在網上搜索資料的時間來學習,也可以分享給身邊好友一起學習。

17
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • 不會安裝Kubernetes學習環境?Mac筆記本上安排一個