Reference article:

The Paging | | Android Developers Android Developers

Android Jetpack component (7)Paging_ 8 to juvenile -CSDN blog

Android Jetpack component Paging usage – source code _Jason_Lee155 -CSDN blog

Android Jetpack Architecture -Paging custom pull-up load more _One X blogs -CSDN blogs

Ppag-ex: Add headers and footers to Paging lists

A list,

Paging load is a common requirement in application development. It can save data traffic and improve application performance. Google has introduced a Paging component, Paging, to make it easier for developers to load pages. Provides a unified solution for several common paging mechanisms.

  • advantage
    • In-memory caching of paging data. This feature ensures that applications use system resources efficiently when processing paging data.
    • The built-in request deduplication function ensures efficient utilization of network bandwidth and system resources.
    • The configurable RecyclerView adapter automatically requests data when the user scrolls to the end of the loaded data.
    • First-class support for Kotlin coroutines and flows, as well as LiveData and RxJava.
    • Built-in support for error handling, including refresh and retry capabilities.
  • Data source: Paging supports three data architecture types
    • Networking: Paging load of network data is the most common requirement. API interfaces are often different, and Paging provides three different schemes for dealing with different Paging mechanisms. Paging does not provide the task error handling function. Network requests can be retried if an error occurs.
    • Database: The paging loading of databases is similar to that of networks. It is recommended to use the Room database to modify and insert data.
    • Network + database: Usually a single data source is used as a solution, fetching data from the network, caching it directly into the database, and lists fetching data directly from the database.

Second, the core

2.1 core classes

The working principle of Paging mainly involves three classes:

  1. PagedListAdapter:RecyclerView.AdapterBase class, used inRecyclerViewfromPagedListPage data of.
  2. PagedList:PagedListResponsible for notifyingDataSourceWhen data is retrieved, such as the first page loaded, the last page loaded, and the number of pages loaded. The data retrieved from the DataSource will be stored inPagedListIn the.
  3. DataSource: Performs specific data loading. Data loading is performed on a worker thread

The relationship and data loading process of the above three classes are shown below:

When a new item is inserted into the database, the DataSource is initialized and the LiveData background thread creates a new PagedList. This new PagedList is sent to the PagedListAdapter of the UI thread. The PagedListAdapter uses DiffUtil to compare the difference between the current Item and the new Item. When the end of contrast, PagedListAdapter by calling RecycleView. Adapter. NotifyItemInserted () insert a new item to the appropriate location

2.2 the DataSource

Paing provides us with three DataSource types, depending on the paging mechanism.

  1. PositionalDataSource

This applies when data can be loaded from any location and the number of target data sources is fixed.

  1. PageKeyedDataSource

Suitable for cases where the data source requests as “pages”. For example, when obtaining data carrying page and pageSize. This article code uses this DataSource

  1. ItemKeyedDataSource

This applies when the next page of target data needs to depend on a field in the last object of the previous page of data as a key, such as the interface for commenting data that carries the parameters since and pageSize.

Three, use,

3.1 Build your own DataSource

DataSource Controls data loading, including initial loading, loading of the previous page data, and loading of the next page data. Here we take PageKeyedDataSource as an example

// The generic parameter has no Key Value. Key is the page identifier, where Long is the data type
class ListDataSource : PageKeyedDataSource<Long, Item>() {
    // Retry the loading parameters
    private var lastLoadParam: Pair<LoadParams<Long>, LoadCallback<Long, Item>>? = null
    // Initialize the load data
    override fun loadInitial(
        params: LoadInitialParams<Long>,
        callback: LoadInitialCallback<Long, Item>
    ) {
        CLog.i(TAG, "loadInitial!!")
        lastLoadParam = null
        refreshListLiveData.postValue(true)
        loadInitListState.postValue(LoadListState.STATE_LOADING)
        dispose()
        fetchList(cId, Id, 0, { data, nextKey ->
            CLog.i(TAG, "loadInitial success callback!!")
            val key = if(nextKey ! = -1L) nextKey else null
            // After a successful callback, data is processed data, Id is the Key used to load data on the previous page, Key is the Key used to load data on the next page, and cId is related to services
            callback.onResult(data, Id, key)
            // The last page of data is loaded for the first time
            loadAfterListState.value = LoadListState.STATE_LOAD_END
            refreshListLiveData.value = false
            loadInitListState.value = LoadListState.STATE_LOAD_END
            isListEmpty.value = data.isEmpty()
        }, {
            CLog.i(TAG, "loadInitial failed callback!!")
            refreshListLiveData.value = false
            networkError.value = true
            // Failed callback with both keys null
            callback.onResult(listOf(), null.null)})}// Load the page data
    override fun loadBefore(
        params: LoadParams<Long>,
        callback: LoadCallback<Long, Item>
    ) {
        CLog.i(TAG, "loadBefore start") loadBeforeListState.postValue(changeLoadState(loadBeforeListState.value!!) ) fetchList(cId, params.key,1, { data, nextKey ->
            CLog.i(TAG, "loadBefore success callback")
            lastLoadParam = null
            val key = if(nextKey ! = -1L) nextKey else null
            // Fill the callback with data and Key
            callback.onResult(data.drop(1).reversed(), key)
            loadBeforeListState.value = LoadListState.STATE_LOAD_END
        }, {
            CLog.i(TAG, "loadBefore failed callback")
            lastLoadParam = params to callback
            loadBeforeListState.value = LoadListState.STATE_LOAD_ERROR
        })
    }
    // Load the next page data
    override fun loadAfter(params: LoadParams<Long>, callback: LoadCallback<Long, Item>) {
        CLog.i(TAG, "loadAfter start") loadAfterListState.postValue(changeLoadState(loadAfterListState.value!!) ) fetchList(categoryId, params.key,0, { data, nextKey ->
            CLog.i(TAG, "loadAfter success callback")
            lastLoadParam = null
            val key = if(nextKey ! = -1L) nextKey else null
            // Fill the callback with data and Key
            callback.onResult(data.drop(1), key)
            loadAfterListState.value = LoadListState.STATE_LOAD_END
        }, {
            CLog.i(TAG, "loadAfter failed callback")
            lastLoadParam = params to callback
            loadAfterListState.value = LoadListState.STATE_LOAD_ERROR
        })
    }
    // The header retry function
    fun retryLoadHead(a) {
        val param = lastLoadParam
        if(param ! =null)
            loadBefore(param.first, param.second)

    }
    // Tail retry function
    fun retryLoadFoot(a) {
        val param = lastLoadParam
        if(param ! =null) {
            loadAfter(param.first, param.second)
        }
    }

    //type: 0 to load lower data, 1 to load upper data
    fun fetchList(
        cId: Int,
        cursor: Long,
        type: Int,
        onComplete: (List<Item>, Long) - >Unit,
        onFail: () -> Unit
    ) {
        // Encapsulate the network request method
        val req = ScanContentListReq().apply {
            this.cId = cId
            scanReq = ScanReq().apply {
                this.cursor = cursor
                limit = PAGE_SIZE
                scanType = type
            }
        }
        scanListRxJava(req)
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(object : Observer<ScanListResp> {
                override fun onSubscribe(d: Disposable){}override fun onNext(resp: ScanListResp) {
                    if(resp.baseResp.error.code ! =0) {
                        onFail()
                        return
                    }
                    if (resp.contentList == null) {
                        onFail()
                        return
                    }
                    val key = if (resp.hasMore) resp.newCursor else -1
                    val data = resp.List
                        .map {
                            val ListItem = Item(it)
                            ListItem
                        }
                    onComplete(data, key)
                }

                override fun onError(e: Throwable) {
                    networkError.value = true
                }

                override fun onComplete(a){}})}}Copy the code

The Key points are that each time the Key is selected and the loadInitial, loadBefore, and loadAfter functions are overwritten. PageKeyedDataSource’s Key generally depends on the data returned by the server.

3.2 build PagedList

companion object{

  private const val TAG = "List"
  const val PAGE_SIZE = 5
  const val FETCH_DIS = 1

}
val ListData: LiveData<PagedList<Item>> = LivePagedListBuilder(
  dataSourceFactory,
  Config(
        PAGE_SIZE,
        FETCH_DIS,
        true
    )
).build()
Copy the code

Where PAGE_SIZE is the number per page and FETCH_DIS is the distance to the last data item before the load action is triggered.

Here ListData is of type LiveData, so you can listen on the Activity and refresh the adapter when data changes:

ListViewModel.ListData.observe(this) {
    adapter.submitList(it)
}
Copy the code

3.3 Build your own PagedListAdapter

Must inherit PagedListAdapter

(‘ ‘POST_COMPARATOR’ ‘), POST_COMPARATOR is DiffUtil, The PagedListAdapter uses DiffUtil to compare the difference between the current Item and the new Item.
,>

typealias ItemClickListener = (Item) -> Unit
typealias onClickListener = () -> Unit

class ListAdapter(
    private val context: Context,
    private val onItemClickListener: ItemClickListener,
    private val retryHeadClickListener: onClickListener,
    private val retryFootClickListener: onClickListener
) : PagedListAdapter<Item, RecyclerView.ViewHolder>(POST_COMPARATOR) {

    companion object {
        private const val TAG = "ListAdapter"
        val POST_COMPARATOR = object : DiffUtil.ItemCallback<Item>() {
            @SuppressLint("DiffUtilEquals")
            override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean =
                oldItem.id == newItem.id

            override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean =
                oldItem.id == newItem.id

            override fun getChangePayload(oldItem: Item, newItem: Item): Any? {
                return null}}const val TYPE_ITEM = 0
        const val TYPE_LOAD_HEAD = 1
        const val TYPE_LOAD_FOOT = 2
    }

    private var loadAfterState = LoadListState.STATE_NONE
    private var loadBeforeState = LoadListState.STATE_NONE

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        when (getItemViewType(position)) {
            TYPE_LOAD_HEAD -> (holder as LoadingHeadViewHolder).bind(loadBeforeState)
            TYPE_LOAD_FOOT -> (holder as LoadingFootViewHolder).bind(loadAfterState)
            else -> {
                (holder as ItemViewHolder).bind(
                    getItem(position - 1)!!!!! , position, Id ) } } }override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        return when (viewType) {
            TYPE_LOAD_FOOT -> LoadingFootViewHolder.create(parent, this, retryFootClickListener)
            TYPE_LOAD_HEAD -> LoadingHeadViewHolder.create(parent, this, retryHeadClickListener)
            else -> ItemViewHolder.create(parent, onItemClickListener)
        }
    }

    private fun isFirstItem(position: Int) = position == 0

    private fun isLastItem(position: Int) = position == (itemCount - 1)

    override fun getItemCount(a) = super.getItemCount() + 2

    override fun getItemViewType(position: Int): Int {
        return when {
            isLastItem(position) -> TYPE_LOAD_FOOT
            isFirstItem(position) -> TYPE_LOAD_HEAD
            else -> TYPE_ITEM
        }
    }
 }
Copy the code

It can be seen that the basic script and ordinary RecyclerView.Adapter is almost the same, but more DiffUtil, use is the same:

adapter = ListAdapter(
    this,
    onItemClickListener,
    headRetryClickListener,
    footRetryClickListener
)
list_rv.adapter = adapter
Copy the code

Fourth, the Paging 3.0

Paging3 is very different from old Paging. Paging2.x works fine, but the code is a bit cumbersome and the functionality isn’t perfect. For example, there’s no pull-down refresh, and you have to call the DataSource#invalidate() method to reset the data. Page 3.0 is more powerful and easier to use.

4.1 the difference between

  • DataSource

Paging3 implements load() and getRefreshKey(). In Paging3, all load method parameters are replaced by a LoadParams sealed class. This class contains subclasses for each load type. If you want to distinguish between load types in load(), you need to check which subclass of LoadParams is passed in

  • PagedListAdapter

Adapter no longer inherits PagedListAdapter, but is replaced by PagingDataAdapter, all else unchanged.

class ArticleAdapter : PagingDataAdapter<Article,ArticleViewHolder>(POST_COMPARATOR){

    companion object{

        val POST_COMPARATOR = object : DiffUtil.ItemCallback<Article>() {
            override fun areContentsTheSame(oldItem: Article, newItem: Article): Boolean =
                oldItem == newItem

            override fun areItemsTheSame(oldItem: Article, newItem: Article): Boolean =
                oldItem.id == newItem.id
        }
    }

        override fun onBindViewHolder(holder: ArticleViewHolder, position: Int){ holder.tvName.text = getItem(position)? .title }override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ArticleViewHolder {
        return ArticleViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.list_item,parent,false))}}class ArticleViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView){
    val tvName: TextView = itemView.findViewById(R.id.tvname)
}
Copy the code

4.2 Obtain data and set it to the Adapter

Google recommended that I use a three-tier architecture to complete the data to Adapter setup, as shown below

The code base layer

The primary Paging library component in the code base layer is PagingSource. Each PagingSource object defines the data source and how to retrieve data from that source. The PagingSource object can load data from any single data source, including network sources and local databases. Another component of the Paging library that can be used is RemoteMediator. The RemoteMediator object handles paging from hierarchical data sources, such as network data sources with a local database cache.

The ViewModel layer

The Pager component provides a public API to construct instances of PagingData exposed in reactive flows based on the PagingSource object and the PagingConfig configuration object. The component that connects the ViewModel layer to the interface is PagingData. The PagingData object is a container for holding PagingData snapshots. It queries the PagingSource object and stores the results.

Interface layer

The main Paging library component in the interface layer is the PagingDataAdapter.