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:
- PagedListAdapter:
RecyclerView.Adapter
Base class, used inRecyclerView
fromPagedList
Page data of. - PagedList:
PagedList
Responsible for notifyingDataSource
When 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 inPagedList
In the. - 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.
- PositionalDataSource
This applies when data can be loaded from any location and the number of target data sources is fixed.
- 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
- 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.