Paging3 of Jetpack series

Paging3 of Jetpack series Introduction

foreword
Paging3 of Jetpack series.I haven't updated the articles in the Jetpack series for a long time. This article shares the use of the paging library Paging3. If you haven't read my other articles on Jetpack, you can move to the link:
Android JetPack series of articles, continuously updated
In order to make myself learn more quickly, I, an English rookie, have been trying to make up for English recently. I will learn faster by reading the original official documents. The emperor pays off. After two weeks of perseverance, now the official document contains 10 I already know two words~

Paging3 of Jetpack series.The source code address of this article has been uploaded: https://github.com/huanglinqing123/PagingDataDemo Welcome to start and issues

Paging3 of Jetpack series.What is Paging


Think about what we need to do to achieve paging loading in our previous business? Generally, we encapsulate RecycleView ourselves or use a third-party library such as XRecycleView to do it, and Paging is the standard library for the paging function provided by Google, so that we do not need to implement the paging function based on RecycleView by ourselves, and Paging provides us with Many configurable options make pagination more flexible . Paging3 is the latest version of the Paging library, and it is still in the beta version. Compared with the use of Paging2, it is much simpler.
Use of Paging

Paging3 of Jetpack series.Project construction


First, we create a new project and refer to the paging library in gradle as follows:
def paging_version = "3.0.0-alpha07"
implementation "androidx.paging:paging-runtime:$paging_version"
testImplementation "androidx.paging:paging-common:$paging_version"

For the project example, we use Kotlin language and use coroutines and Flow, so the libraries that also need to add coroutines are as follows:
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.7-mpp-dev-11'

Paging3 of Jetpack series.If you don't know about coroutines and Flow, you can move to the first two articles
This article is enough for Kotlin coroutines
Kotlin Flow see this article to get you started~
Project example
The use diagram of our Paging in the architecture is also given in the official document

From the above figure, we can also clearly see that Paging has specific performance in the warehouse layer, ViewModel and UI layer. Next, we will explain step by step how Paging works in the project architecture through an example.
API interface preparation
the query interface in "playing Android" of "Hongyang " boss: https://wanandroid.com/wenda/list/1/json
Here we have written the service code of the RetrofitService class to create network requests as follows:
object RetrofitService {
/**
* okhttp client
*/
lateinit var okHttpClient: OkHttpClient
/**
* Main Url address
*/
private const val BASEAPI = "https://www.wanandroid.com/";
/**
* Create service object
*/
fun createService(mClass: Class): T {
val builder: OkHttpClient.Builder = OkHttpClient.Builder();
okHttpClient = builder.build()

val retrofit: Retrofit = Retrofit.Builder()
.baseUrl(BASEAPI)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
return retrofit.create(mClass) as T
}
}

Interface with DataApi, here we declare the method as a suspend function, which is convenient to call in the coroutine
interface DataApi {
/**
* retrieve data
*/
@GET("wenda/list/{pageId}/json")
suspend fun getData(@Path("pageId") pageId:Int): DemoReqData
}

define data source
Paging3 of Jetpack series.First let's define the data source DataSource inherits from PagingSource, the code is as follows:
class DataSource():PagingSource(){
override suspend fun load(params: LoadParams): LoadResult {
TODO("Not yet implemented")
}
}

We can see that there are two parameters Key and Value in PagingSource, where Key is defined as Int type Value DemoReqData is the entity class corresponding to the data returned by the interface, which means
We pass the value of Int type (such as page number) to get the returned data information DemoReqData object.
It should be reminded here that if you are not using Kotlin coroutines but Java, you need to inherit the corresponding PagingSource such as RxPagingSource or ListenableFuturePagingSource.
DataSource automatically generates the load method for us, and our main request operation is done in the load method. The main code looks like this:

override suspend fun load(params: LoadParams): LoadResult {
return try {
//The page number is undefined and set to 1
var currentPage = params.key?: 1
/ / Warehouse layer request data
var demoReqData = DataRespority().loadData(currentPage)
//The current page number is less than the total page number page plus 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)
}
}

Paging3 of Jetpack series.In the above code, we can see that in the datasource, we pass the DataRespority() warehouse layer to request data, if there is no more data, return null, and finally use LoadResult.Page to return the result, if the loading fails, use LoadResult.Error to return, because The data in LoadResult.Page must be of a non-null type, so we need to determine whether the return is null.
Next, let's look at the code of the DataRespority warehouse layer. The code is relatively simple, as shown below:
class DataRespority {
private var netWork = RetrofitService.createService(
DataApi::class.java
)
/**
* Query nursing data
*/
suspend fun loadData(
pageId: Int
): DemoReqData? {
return try {
netWork.getData(pageId)
} catch (e: Exception) {
//handle or catch exception here
null
}
}
}

The official process of Load calling is as follows:

As can be seen from the above figure, the load method is automatically triggered by us through the configuration of Paging, and we do not need to call it every time, so how do we use DataSource?
call 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.
The translation of this sentence means: The Pager object calls the load() method from the PagingSource object, provides it with a LoadParams object, and receives a LoadResult object in return.
So we are creating a viewModel object, and creating a pager object to call the PagingSource method, the code is as follows:
class MainActivityViewModel : ViewModel() {
/**
* retrieve data
*/
fun getData() = Pager(PagingConfig(pageSize = 1)) {
DataSource()
}.flow
}

Paging3 of Jetpack series.In the viewmodel, we define a getData method. The Pager implements special customization by configuring PagingConfig. Let's take a look at the parameters in PagingConfig as follows:
pageSize: Defines the number of items to load from the PagingSource at a time.
prefetchDistance: The prefetch distance, the simple explanation is that the next page is automatically loaded when it is far from the bottom, that is, the load method is automatically called, and the default value is equal to pageSize
enablePlaceholders: Whether to display placeholders, when the network is not good, the framework of the page can be considered to improve the user experience

There are some other parameters that are not introduced here. From the source code of the construction method, we can see that the pageSize parameter is required, and the others are optional, so we pass 1 here.
Define RecycleViewAdapter
This step is not much different from the ordinary RecycleViewAdapter we usually define, but we inherit the PagingDataAdapter. The main code is as follows:
class DataRecycleViewAdapter :
PagingDataAdapter(object :
DiffUtil.ItemCallback() {
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
}
}

What we want to remind here is the DiffUtil parameter, a callback used to calculate the difference between two non-empty items in the list. Unless there are special circumstances, it is generally a fixed spelling.
View layer data request and display the result on the View
At this point, the basic work is almost done. Of course, we are almost there, but we can see the results soon. There are many places that need to be explained. In the last step, we request data in the view and bind the results to the adapter.
We call the getData method in the viewModel in the View code, the code is as follows:
val manager = LinearLayoutManager(this)
rv_data.layoutManager=manager
rv_data.adapter = dataRecycleViewAdapter
btn_get.setOnClickListener {
lifecycleScope.launch {
mainActivityViewModel.getData().collectLatest {
dataRecycleViewAdapter.submitData(it)
}
}
}

We call the getData method in the coroutine, receive the latest data, and provide the adapter with data through the submitData method of the PagingAdapter. The running result is as follows (ignore the ugly UI.jpg)

When we swipe down, the next page will be automatically loaded when there is 1 (pageSize) data left at the bottom.
Of course, there is no need to pass pageSize for this interface, so the returned data size will not be affected by pageSize. In this way, we use Paging3 to complete a simple data paging request.
Loading state of Paging
Paging3 provides us with a way to get the loading state of Paging, including the way to add listening events and the way to display them directly in the adapter. First, let's look at the way to listen to events
Get loading status by listening to events
Above we created the dataRecycleViewAdapter in the Activity to display the page data, we can use the addLoadStateListener method to add the listening event of the loading state, as shown below:
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")
}
}
}

The it here is the CombinedLoadStates data class, with refresh, Append, and Prepend. The differences are shown in the following table:
refreshUse of refresh at initialization
appenduse when loading more
prependUsed when adding data to the head of the current list
That is to say, if the monitoring is it.refresh, when the second page and the third page are loaded, the status cannot be monitored. Here we only take it.refresh as an example.

There are three values of LoadState, namely NotLoading: when there is no loading action and there is no error
Loading and Error, as the names suggest, correspond to loading and loading errors. In addition to addLoadStateListener, the monitoring method can also directly use the loadStateFlow method. Since the flow is a suspend function, we need to execute it in the coroutine ( Kotlin Flow see this A guide to get you started~ ), the code is as follows:
lifecycleScope.launch {
dataRecycleViewAdapter.loadStateFlow.collectLatest {
when (it.refresh) {
is LoadState.NotLoading -> {
}
is LoadState.Loading -> {
}
is LoadState.Error -> {
}
}
}
}

Next, we run the example in the previous section. After the operation is successful, click the query button to display the data. Let's see the print as follows:
2020-11-14 16:39:19.841 23729-23729/com.example.pagingdatademo D/MainActivity: is NotLoading
2020-11-14 16:39:24.529 23729-23729/com.example.pagingdatademo D/MainActivity: Query button clicked
2020-11-14 16:39:24.651 23729-23729/com.example.pagingdatademo D/MainActivity: is Loading
2020-11-14 16:39:25.292 23729-23729/com.example.pagingdatademo D/MainActivity: is NotLoading

The first is the NotLoading state, because we have not done anything. After clicking the query button, it becomes the Loading state because the data is being loaded. After the query is completed, it returns to the NotLoading state again, which is in line with our expectations. What is the use of this state? We Displaying a progressBar transition in the Loading state improves the user experience, etc. Of course, the most important thing is the Error state, because we need to inform the user in the Error state.
We reopen the App, disconnect the network connection, click the query button again, and print the log as follows:
2020-11-14 16:48:25.943 26846-26846/com.example.pagingdatademo D/MainActivity: is NotLoading
2020-11-14 16:48:27.218 26846-26846/com.example.pagingdatademo D/MainActivity: Query button clicked
2020-11-14 16:48:27.315 26846-26846/com.example.pagingdatademo D/MainActivity: is Loading
2020-11-14 16:48:27.322 26846-26846/com.example.pagingdatademo D/MainActivity: is Error

What should be noted here is that the status of this Error is not automatically returned by Paging for us, but informed by the LoadResult.Error method after we catch the exception in the DataSource.

Paging3 of Jetpack series.We also need to monitor specific errors in the Error state. If there is no network, it will display no network UI. If the server is abnormal, it will prompt the server to be abnormal. The code is as follows:

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")
}
}
}

When we are disconnected from the network, click query, and the log is as follows:
2020-11-14 17:29:46.234 12512-12512/com.example.pagingdatademo D/MainActivity: Query button clicked
2020-11-14 17:29:46.264 12512-12512/com.example.pagingdatademo D/MainActivity: Requesting page 1
2020-11-14 17:29:46.330 12512-12512/com.example.pagingdatademo D/MainActivity: is Loading
2020-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

show in adapter
Paging3 provides us with methods to add bottom and header adapters, namely withLoadStateFooter, withLoadStateHeader, and withLoadStateHeaderAndFooter to add both header and footer methods. Here we take adding the footer method as an example.
First we create the viewHolder LoadStateViewHolder binding layout is the layout displayed at the bottom, a loading display and a retry button, the xml layout is as follows so:

xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
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">
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="正在加载数据... ..."
android:textSize="18sp" />
android:layout_width="20dp"
android:layout_height="20dp" />

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" />



The layout of the loading prompt and re-request is hidden by default, and the LoadStateViewHolder code is as follows:
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
}
}
}

We are divided into two classes with Adapter, so we have to pass the parent in the adapter as a parameter, and retry() is a higher-order function, which is convenient for retrying logic in the adapter after clicking to retry.
bindState is to set data and display different UI according to the state of State.
Next, let's create a LoadStateFooterAdapter that inherits from LoadStateAdapter, and the corresponding viewHolder is LoadStateViewHolder. The code is as follows:
class LoadStateFooterAdapter(private val retry: () -> Void) :
LoadStateAdapter() {
override fun onBindViewHolder(holder: LoadStateViewHolder, loadState: LoadState) {
(holder as LoadStateViewHolder).bindState(loadState)
}
override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): LoadStateViewHolder {
return LoadStateViewHolder(parent, retry)
}
}

The code here is relatively simple, so I won't explain it. Finally, let's add this adapter
rv_data.adapter =
dataRecycleViewAdapter.withLoadStateFooter(footer = LoadStateFooterAdapter(retry = {
dataRecycleViewAdapter.retry()
}))

It should be noted here that the adapter returned by withLoadStateFooter should be set to the recyclerview. If you write: dataRecycleViewAdapter.withLoadStateFooter and then set the adapter of the recycleView separately, it will have no effect.
Here we click to retry the retry() method of dataRecycleViewAdapter. After we run the program to ask for help on the first page, disconnect the network, and then scroll down. The effect is as follows:

In this way, we have completed the display of the data loading status in the adapter.
In addition, there is a more important RemoteMediator in Paging3, which is used to better load the network database and local database. We will have the opportunity to share it with you later~
Updated November 21, 2020
The design concept of paging3 is that it is not recommended to modify the list data directly; instead, it operates on the data source, and the changes of the data source will be automatically updated to the list. I saw that many friends in the comment area said how to delete and modify items. Here we use The easiest way
Modifications to a single item
We all know that there is no Api that directly monitors item monitoring in RecycleView. Generally, it is fetched in onBindViewHolder, or operated at the View layer through callbacks. The callback can also be written as a higher-order function here. We call back to the View layer here. The reason is that a partner in the comment area commented that the viewModel should be manipulated, so to avoid injecting the viewModel into the adapter, we can directly use a higher-order function callback. Modify the DataRecycleViewAdapter code as follows:
class DataRecycleViewAdapter(
val itemUpdate: (Int, DemoReqData.DataBean.DatasBean?,DataRecycleViewAdapter) -> Unit
) :
PagingDataAdapter(object :
DiffUtil.ItemCallback() {
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
}
}

In order to facilitate the demonstration, we have added a button to update the data in the data list. The code for declaring the adapter in the Activity is modified as follows:
private var dataRecycleViewAdapter = DataRecycleViewAdapter { position, it, adapter ->
it?.author = "Huang Linqing${position}"
adapter.notifyDataSetChanged()
}

We change the author's name to Huang Linqing and the currently clicked serial number by executing the high-order function, and then call notifyDataSetChanged. The demonstration effect is as follows:

Deletion and addition of data
We all know that before, we set up a List for the adapter. If we need to delete or add new ones, we just need to change the List, but there seems to be no way in Paging3, because the data source is PagingSource , read the introduction of the official website

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.

The general meaning is that if the data changes, a new PagingData must be created, so for the time being, I don't know how to delete or add data and then refresh it without re-requesting. If you have a good solution, please let me know!

Related Articles

Explore More Special Offers

  1. Short Message Service(SMS) & Mail Service

    50,000 email package starts as low as USD 1.99, 120 short messages start at only USD 1.00