孟老板 Paging3 (一) 入門


 

 

Paging3 (一)  入門

前言: 

官方分頁工具,  確實香.   但數據源不開放, 無法隨意增刪改操作;  只能借助 Room;  但列表數據不一定都要用 Room吧;  

如果偏查詢的分頁數據用 Paging3 ;  其他一概用 老Adapter;  這倒也算個方案. [苦笑]

 

目錄:

  1. 簡單使用  -  數據源,Viewmodel,Adapter 等
  2. LoadResult  -  Error, Page.  Error 用法等
  3. PagingConfig
  4. 監聽列表加載狀態
  5. LoadStateAdapter  -  loading, 加載失敗, 沒有更多等
  6. Map  -  數據預處理

 

官方 Pagings 優勢: 

  • 分頁數據的內存中緩存。該功能可確保您的應用在處理分頁數據時高效利用系統資源。
  • 內置的請求重復信息刪除功能,可確保您的應用高效利用網絡帶寬和系統資源。
  • 可配置的 RecyclerView 適配器,會在用戶滾動到已加載數據的末尾時自動請求數據。
  • 對 Kotlin 協程和 Flow 以及 LiveData 和 RxJava 的一流支持。
  • 內置對錯誤處理功能的支持,包括刷新和重試功能。

導包:

dependencies {
  val paging_version = "3.0.0"

  //唯一必導包
  implementation("androidx.paging:paging-runtime:$paging_version")

  // 測試用
  testImplementation("androidx.paging:paging-common:$paging_version")

  // optional - RxJava2 support
  implementation("androidx.paging:paging-rxjava2:$paging_version")

  // optional - RxJava3 support
  implementation("androidx.paging:paging-rxjava3:$paging_version")

  // 適配 Guava 庫  - 高效java擴展庫
  implementation("androidx.paging:paging-guava:$paging_version")

  // 適配 Jetpack Compose  - 代碼構建View; 干掉 layout
  implementation("androidx.paging:paging-compose:1.0.0-alpha09")
}

 

1. 簡單使用:

1.1 數據源  PagingSource 

自定義數據源, 繼承 PagingSource 

它有兩個泛型參數,  1. 頁碼key,  沒有特殊需求的話一般就是 Int 類型;  2.集合實體類型

重寫兩個方法:  1.load()  加載數據的方法;   2.getRefreshKey  初始加載的頁碼;  暫且返回 1 或 null

LoadResult.Page 后面再講;

class DynamicDataSource: PagingSource<Int, DynamicTwo>() {

    //模擬最大頁碼
    private var maxPage = 2

    //模擬數據
    private fun fetchItems(startPosition: Int, pageSize: Int): MutableList<DynamicTwo> {
        Log.d("ppppppppppppppppppppp", "startPosition=${startPosition};;;pageSize=${pageSize}")
        val list: MutableList<DynamicTwo> = ArrayList()
        for (i in startPosition until startPosition + pageSize) {
            val concert = DynamicTwo()
            concert.title = "我是標題${i}"
            concert.newsInfo = "我是內容${i}"
            concert.nickName = "小王${i}"
            list.add(concert)
        }
        return list
    }

    override fun getRefreshKey(state: PagingState<Int, DynamicTwo>): Int? = null

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, DynamicTwo> {
        val nextPageNumber = params.key ?: 1
        val size = params.loadSize
        Log.d("ppppppppppppppppppppp", "nextPageNumber=${nextPageNumber};;;size=${size}")
        val response = fetchItems((nextPageNumber-1) * size, size)

        return LoadResult.Page(
            data = response,
            prevKey = null, // Only paging forward.  只向后加載就給 null
            //nextKey 下一頁頁碼;  尾頁給 null;  否則當前頁碼加1
            nextKey = if(nextPageNumber >= maxPage) null else (nextPageNumber + 1)
        )
    }
}

 

1.2 ViewModel

代碼比較簡單.  內容我們一會再講

class DynamicPagingModel(application: Application) : AndroidViewModel(application) {
    val flow = Pager(
        //配置
        PagingConfig(pageSize = 10, prefetchDistance = 2,initialLoadSize = 10)
    ) {
        //我們自定義的數據源
        DynamicDataSource()
    }.flow
        .cachedIn(viewModelScope)
}

 

1.3 前台使用: 

初始化 Adapter 及 RecycleView

mViewModel?.flow?.collectLatest  綁定監聽,  然后通過 submitData() 刷新列表;

mAdapter = SimplePagingAdapter(R.layout.item_dynamic_img_two, null)

mDataBind.rvRecycle.let {
    it.layoutManager = LinearLayoutManager(mActivity)
    it.adapter = mAdapter
}

//Activity 用 lifecycleScope
//Fragments 用 viewLifecycleOwner.lifecycleScope
viewLifecycleOwner.lifecycleScope.launchWhenCreated {
    mViewModel?.flow?.collectLatest {
        mAdapter.submitData(it)
    }
}

 

1.4 Adapter

必須繼承  paging 的 PagingDataAdapter 

DiffCallback() 或 handler  NewViewHolder 不了解的可以看我的 ListAdapter 封裝系列 

open class SimplePagingAdapter(
    private val layout: Int,
    protected val handler: BaseHandler? = null
) :
    PagingDataAdapter<DynamicTwo, RecyclerView.ViewHolder>(DiffCallback()) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        return NewViewHolder(
            DataBindingUtil.inflate(
                LayoutInflater.from(parent.context), layout, parent, false
            ), handler
        )
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        if(holder is NewViewHolder){
            holder.bind(getItem(position))
        }
    }
}

over  簡單的分頁模擬數據已完成;  

 

2. LoadResult

它是一個密封類;   它表示加載操作的結果;

2.1 LoadResult.Error

表示加載失敗;  需提供 Throwable 對象.  

public data class Error<Key : Any, Value : Any>(
    val throwable: Throwable
) : LoadResult<Key, Value>()

可用於:

  • 異常時返回,  HTTP, IO, 數據解析等異常; 
  • 服務器錯誤碼響應
  • 沒有更多數據

 

2.1 LoadResult.Page

表示加載成功;   

參數:

data 數據集合;  

prevKey 前頁頁碼 key;   //向下一頁加載 給null

nextKey 后頁頁碼 key;    //向上一頁加載 給null 

public data class Page<Key : Any, Value : Any> constructor(
/**
* Loaded data
*/
val data: List<Value>,
/**
* [Key] for previous page if more data can be loaded in that direction, `null`
* otherwise.
*/
val prevKey: Key?,
/**
* [Key] for next page if more data can be loaded in that direction, `null` otherwise.
*/
val nextKey: Key?,
/**
* Optional count of items before the loaded data.
*/
@IntRange(from = COUNT_UNDEFINED.toLong())
val itemsBefore: Int = COUNT_UNDEFINED,
/**
* Optional count of items after the loaded data.
*/
@IntRange(from = COUNT_UNDEFINED.toLong())
val itemsAfter: Int = COUNT_UNDEFINED
) : LoadResult<Key, Value>() {

 

3.PagingConfig

分頁配置

參數:

pageSize:  每頁容量

prefetchDistance:  當RecycleView 滑動到底部時, 會自動加載下一頁.   如果能提前預加載, 可以省去部分等待加載的時間.

        prefetchDistance 就是距離底部提前加載的距離.  默認 = pageSize;   = 0 時將不會加載更多

 

enablePlaceholders:  允許使用占位符.  想了解的點這里

initialLoadSize: 初始加載數量,  默認 = pageSize * 3

maxSize:   似乎意義沒有那么簡單.  還沒看源碼,不清楚;  不能 < pageSize + prefetchDistance * 2

jumpThreshold: 某閾值!  好吧我攤牌了, 我不知道. [奸笑]

 

4.監聽加載狀態:  

LoadState:  表示加載狀態密封類;

LoadState.NotLoading:  加載完畢,  並且界面也已相應更新

LoadState.Error:  加載失敗. 

LoadState.Loading:  正在加載..

lifecycleScope.launch {
    mAdapter.loadStateFlow.collectLatest { loadStates ->
        when(loadStates.refresh){
            is LoadState.Loading -> {
                Log.d("pppppppppppppp", "加載中")
            }
            is LoadState.Error -> {
                Log.d("pppppppppppppp", "加載失敗")
            }
            is LoadState.NotLoading -> {
                Log.d("pppppppppppppp", "完事了")
            }
            else -> {
                Log.d("pppppppppppppp", "這是啥啊")
            }
        }
    }

    //或者:
    mAdapter.addLoadStateListener { ... }
}

 

5. 狀態適配器  LoadStateAdapter

 用於直接在顯示的分頁數據列表中呈現加載狀態。 例如:  尾部顯示 正在加載, 加載失敗, 沒有更多等;

 

5.1 自定義 MyLoadStateAdapter  繼承 LoadStateAdapter

重寫  onCreateViewHolder,  onBindViewHolder 

retry:  如果加載失敗, 想要重試,  則提供該高階函數參數;  否則不需要它

class MyLoadStateAdapter(
    /**
     * 當下一頁加載失敗時, 繼續嘗試加載下一頁; 
     */
    private val retry: () -> Unit
) : LoadStateAdapter<LoadStateViewHolder>() {

    override fun onCreateViewHolder(
        parent: ViewGroup,
        loadState: LoadState
    ) = LoadStateViewHolder(parent, retry)

    override fun onBindViewHolder(
        holder: LoadStateViewHolder,
        loadState: LoadState
    ) = holder.bind(loadState)
}

 

5.2 自定義 LoadStateViewHolder

功能: 

  • 加載中 顯示 Loading;
  • 加載失敗  顯示 錯誤信息.    包括 http, IO 異常,  后台給的錯誤 msg 等;
  • 沒有更多
class LoadStateViewHolder (
    parent: ViewGroup,
    retry: () -> Unit
) : RecyclerView.ViewHolder(
    LayoutInflater.from(parent.context)
        .inflate(R.layout.view_loading_more, parent, false)
) {
    private val binding = ViewLoadingMoreBinding.bind(itemView)

    init {
        //當點擊重試按鈕時, 調用 PagingDataAdapter 的 retry() 重新嘗試加載
        binding.btnLoadingRetry.setOnClickListener {
            retry()
        }
    }

    fun bind(loadState: LoadState) {
        // 當加載失敗時.
        if(loadState is LoadState.Error){
            // 將沒有更多封裝成 NoMoreException;  此時顯示沒有更多 View
            if(loadState.error is NoMoreException){
                hideNoMoreUi(false) //顯示 沒有更多 View
                hideErrUi(true)     //隱藏 失敗 View
            }else{
                hideNoMoreUi(true)
                hideErrUi(false, loadState.error.message)   //顯示失敗 View時, 填充錯誤 msg
            }
        }else{
            hideNoMoreUi(true)
            hideErrUi(true)
        }

        //加載中..
        binding.pbLoadingBar.visibility = if(loadState is LoadState.Loading){
            View.VISIBLE
        }else{
            View.GONE
        }
    }

    /**
     * 隱藏沒有更多View;
     */
    private fun hideNoMoreUi(hide: Boolean){
        if(hide){
            binding.tvLoadingHint.visibility = View.GONE
        }else{
            binding.tvLoadingHint.visibility = View.VISIBLE
        }
    }

    /**
     * 隱藏 加載失敗View;
     */
    private fun hideErrUi(hide: Boolean, msg: String? = null){
        if(hide){
            binding.tvLoadingError.visibility = View.GONE
            binding.btnLoadingRetry.visibility = View.GONE
        }else{
            binding.tvLoadingError.text = msg
            binding.tvLoadingError.visibility = View.VISIBLE
            binding.btnLoadingRetry.visibility = View.VISIBLE
        }
    }
}

 

順便補一下  NoMoreException;  用法? 在下面 PagingSource 嘍.

class NoMoreException: RuntimeException()

 

5.3 layout  view_loading_more.xml

包含:   TextView: 沒有更多;    ProgressBar: 加載中;   TextView: 錯誤信息;   Button: 重試按鈕

<layout>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:paddingHorizontal="16dp"
    android:layout_width="match_parent"
    android:layout_height="54dp">
    <View
        android:layout_width="match_parent"
        android:layout_height="1px"
        android:background="#e5e5e5"
        app:layout_constraintTop_toTopOf="parent"/>
    <TextView
        android:id="@+id/tv_loading_hint"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="14sp"
        android:textColor="#798080"
        android:text="已經到底了"
        android:visibility="gone"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"/>
    <ProgressBar
        android:id="@+id/pb_loading_bar"
        android:layout_width="32dp"
        android:layout_height="32dp"
        android:visibility="gone"
        android:indeterminateTint="#7671F8"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"/>
    <TextView
        android:id="@+id/tv_loading_error"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:textColor="@color/shape_red"
        android:text="錯誤信息"
        android:layout_marginEnd="8dp"
        android:maxLines="2"
        android:ellipsize="end"
        android:visibility="gone"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@id/btn_loading_retry"
        app:layout_constraintStart_toStartOf="parent"/>
    <Button
        android:id="@+id/btn_loading_retry"
        android:layout_width="60dp"
        android:layout_height="38dp"
        android:textColor="@color/white"
        android:text="重試"
        android:visibility="gone"
        android:background="@drawable/shape_blue_7671f8_r8"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

 

5.4 PagingSource 需要根據情況  返回不同的  LoadResult

代碼如下,  直接看注釋就可以了;   

class DynamicDataSource: PagingSource<Int, DynamicTwo>() {

    private var maxPage = 1

    override fun getRefreshKey(state: PagingState<Int, DynamicTwo>): Int? = null

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, DynamicTwo> {
        try {
            val nextPageNumber = params.key ?: 1

            //超過頁碼時,  返回沒有更多狀態 NoMoreException
            if(nextPageNumber > maxPage){
                return LoadResult.Error(NoMoreException())
            }

            //這是 Retrofit 網絡請求
            val map = mapOf("page" to nextPageNumber, "pageSize" to params.loadSize)
            val param = ApiManager.INSTANCE.getJsonBody(map)
            val response = ApiManager.INSTANCE.mApi.getDynamicList(param)

            //后台 響應錯誤碼時;  用 RuntimeException 返回錯誤信息
            if(response.code != 200){
                return LoadResult.Error(RuntimeException(response.msg))
            }

            //解析響應數據
            val jo = response.data
            val list = jo?.getAsJsonArray("newsList")?.toString()?.toBeanList<DynamicTwo>() ?: mutableListOf()
            maxPage = jo?.get("totalPage").toString().toInt()

            //返回正常數據
            return LoadResult.Page(
                data = list,
                prevKey = null, // Only paging forward. 只向后加載就給null
                // nextKey 下一頁頁碼;  尾頁給 null;  否則當前頁碼加1
                nextKey = nextPageNumber + 1
            )
        } catch (e: IOException) {
            // IOException for network failures.
            return LoadResult.Error(e)
        } catch (e: HttpException) {
            // HttpException for any non-2xx HTTP status codes.
            return LoadResult.Error(e)
        } catch (e: Exception) {
            // IOException for network failures.
            return LoadResult.Error(e)
        }
    }
}

代碼中 請求參數只給了 page 和 pageSize;  其他參數怎么給?  

  1. DynamicDataSource 的構造方法傳入;  
  2. 動態參數怎么辦?  寫回調, 從ViewModel 中組裝請求數據
  3. 麻煩怎么辦?  創建 BaseDataSource.  將相似代碼封裝.  請求參數通過高階函數從ViewModel組裝; 

 

5.5 前台使用: 

首先正常初始化 Adapter,  RecycleView,  並調用  mViewModel?.flow?.collectLatest

其次  RecycleView 的 adaper 不要給 主數據Adapter;  而是給 withLoadStateFooter() 返回的 ConcatAdapter

val stateAdapter = mAdapter.withLoadStateFooter(MyLoadStateAdapter(mAdapter::retry))
mDataBind.rvRecycle.let {
    it.layoutManager = LinearLayoutManager(mActivity)
    // ****  這里不要給 mAdapter(主數據 Adapter);  而是給 stateAdapter ***
    it.adapter = stateAdapter
}

PagingDataAdapter withLoadStateFooter 方法會返回一個新的 ConcatAdapter 對象; 請將這個 ConcatAdapter 設置給 RecycleView
withLoadStateFooter 的參數 就是我們自定義的 MyLoadStateAdapter; retry -> mAdapter.retry()

5.6 看一下  LoadStateAdapter 的源碼;  

可以發現,  這是個單條目 Adapter.   

並且  只有當  LoadState.Loading, LoadState.Error 時才會出現;   當然也可以重寫  displayLoadStateAsItem(), 讓它所有狀態都出現; 

當 列表狀態變化時,  會設置 loadState 參數;  動態增刪改 Item;

abstract class LoadStateAdapter<VH : RecyclerView.ViewHolder> : RecyclerView.Adapter<VH>() {

    var loadState: LoadState = LoadState.NotLoading(endOfPaginationReached = false)
        set(loadState) {
            if (field != loadState) {
                val oldItem = displayLoadStateAsItem(field)
                val newItem = displayLoadStateAsItem(loadState)

                if (oldItem && !newItem) {
                    notifyItemRemoved(0)
                } else if (newItem && !oldItem) {
                    notifyItemInserted(0)
                } else if (oldItem && newItem) {
                    notifyItemChanged(0)
                }
                field = loadState
            }
        }

    final override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
        return onCreateViewHolder(parent, loadState)
    }

    final override fun onBindViewHolder(holder: VH, position: Int) {
        onBindViewHolder(holder, loadState)
    }

    final override fun getItemViewType(position: Int): Int = getStateViewType(loadState)

  //條目數量, final 不可重寫;
final override fun getItemCount(): Int = if (displayLoadStateAsItem(loadState)) 1 else 0 abstract fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): VH abstract fun onBindViewHolder(holder: VH, loadState: LoadState) open fun getStateViewType(loadState: LoadState): Int = 0   
  //只有當 Loading, Error 時, 才顯示 open fun displayLoadStateAsItem(loadState: LoadState): Boolean {
return loadState is LoadState.Loading || loadState is LoadState.Error } }

 

5.7 LoadStateAdapter  改建頭尾

如果我們把它強行改造成 Header footer: 

  1. 重寫 displayLoadStateAsItem() 不管什么狀態, 都返回true
  2. loadState 不能重寫,  所以 notifyItemChanged(0) 必被調用; 
  3. 暴力一點, 直接重寫 notifyItemChanged() 讓它什么都不做?   好吧  它也是 final, 不能重寫
  4. 既然要調刷新, 那就調吧 [破涕為笑];  那怎么辦 盡量少執行無用代碼唄,   那就 onBindViewHolder() 啥也不干;
  5. 頭尾由前端控制,  Adapter 只需要把這個 固定View顯示就 ok 了
  6. 如果能阻止 notifyItemChanged(0) 那就更好了.  聰明的你有沒有辦法呢. [666]

最終 Adapter: 

class EndViewAdapter(val v: View) : LoadStateAdapter<EndHolder>() {

    override fun onCreateViewHolder(
        parent: ViewGroup,
        loadState: LoadState
    ) = EndHolder(v)

    override fun onBindViewHolder(holder: EndHolder, loadState: LoadState){
        //啥也不干
    }

    override fun displayLoadStateAsItem(loadState: LoadState) = true
}

class EndHolder(itemView: View) : RecyclerView.ViewHolder(itemView)

 好吧,  一運行, 崩了 [捂臉];   called attach on a child which is not detached 

怎么辦, 取消 RecycleView 的刷新閃爍動畫:   

(mDataBind.rvRecycle.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false;  

整個 RecycleView 的條目刷新動畫都沒了,  這不是個事啊!  但博主已經沒辦法了 [捂臉]

沒辦法了怎么辦? 不用 Header 了?   當然不是,  我們只是不用 LoadStateAdapter 做頭尾了;  我們用 ConcatAdapter 做頭尾;

就是在 withLoadState...  之后,  再自己組裝  ConcatAdapter

 

6. MAP:  數據轉換;  有的時候, 我們需要對響應數據 進行預先處理; 

例如: 根據條件,預先改變實體內容; 

 

val flow: Flow<PagingData<DynamicTwo>> = Pager(
    PagingConfig(pageSize = 10, prefetchDistance = 2,initialLoadSize = 10)
) {
    DynamicDataSource()
}.flow
    .cachedIn(viewModelScope)
    .map {
        it.map { entity ->
            // 這里根據條件,  預先處理數據
            if(entity.isLike == 1){
                entity.nickName = "變變變, 我是百變小魔女"
            }else{
                entity.nickName = "嗚哈哈哈"
            }
            entity
        }
    }

 

例如:  組合實體; 根據條件產生不同實體;

val flow: Flow<PagingData<GroupEntity>> = Pager(
    PagingConfig(pageSize = 10, prefetchDistance = 2,initialLoadSize = 10)
) {
    DynamicDataSource()
}.flow
    .cachedIn(viewModelScope)
    .map {
        it.map { entity ->
            // 這里根據條件,  預先處理數據
            if(entity.isLike == 1){
                GroupEntity.DynamicTwoItem(entity)
            }else{
                GroupEntity.DynamicItem(DynamicEntity())
            }
        }
    }

sealed class GroupEntity{
    class DynamicTwoItem (val entity: DynamicTwo): GroupEntity()
    class DynamicItem (val entity: DynamicEntity): GroupEntity()
}

又例如: 插入實體分隔符等

 

Over

回到頂部

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM