
Introduction to Paging 3
Paging is a paging loading library for Android platform
In fact, Paging is not just launched now, but two versions have been launched before.
But Paging 3 has changed a lot from the previous two versions, and it can even be said to be completely different things. So even if you haven’t learned the usage of Paging before, it doesn’t matter, just treat Paging 3 as a brand new library to learn.
The paging function on Android itself is not difficult to achieve, even if there is no Paging library, we can do it completely, so why does Paging 3 so complicated?
Yes, Paging 3 is complicated, at least if you don’t know it yet
After understanding Paging 3, you will find that it provides a very reasonable paging architecture. We only need to write business logic according to the architecture it provides, and then we can easily implement the paging function. I hope that after reading this article, you can also feel that Paging 3 is fragrant.
The complexity of Paging 3 is that it is associated with too many other knowledge, such as coroutines, Flow, MVVM, RecyclerView , DiffUtil, etc.
In addition, since Paging 3 is a library newly rewritten by Google based on Kotlin coroutines, it is mainly applied to the Kotlin language (Java can also be used, but it will be more complicated), and there will be more and more such libraries in the future, such as Jetpack Compose and more.
Getting started with Paging 3
The best way to learn is to get started directly and use Paging 3 to do a project. After the project is finished, you will basically master it. In this article, we will go this way
In addition, I believe that everyone should have done the paging function before, as I said, this function is not difficult to achieve. But now, please completely forget the paging scheme you are familiar with in the past, because it not only does not help you understand Paging 3, but it will affect your understanding of Paging 3 to a large extent.
Yes, don’t think about listening to the list sliding event, and when you slide to the bottom, initiate a network request to load the next page of data. Paging 3 is not used like this at all. If you still retain this past implementation idea, you will be very hindered when learning Paging 3.
So let’s get started now.
First create a new Android project, here I named it Paging3Sample.
Next, we add the necessary dependencies to the dependencies of build.gradle:
dependencies {
...
implementation 'androidx.paging:paging-runtime:3.0.0-beta01'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
}
Code language: JavaScript (javascript)
Note that although I just said that Paging 3 is to work with many other associated libraries, we do not need to manually import these associated libraries one by one. After Paging 3 is imported, all associated libraries will be automatically downloaded. .
In addition, the Retrofit library is also imported here, because later we will request data from the network and display it in paging through Paging 3.
So before we officially start to use Paging 3, let’s build the network-related code to provide paging data for Paging 3.
Here I am going to use GitHub’s public API as the data source for our project
We can try to request the following interface address in the browser:
https://api.github.com/search/repositories?sort=stars&q=Android&per_page=5&page=1
Code language: JavaScript (javascript)
This interface indicates that all Android-related open source libraries on GitHub will be returned, sorted by the number of stars, and each page will return 5 pieces of data. The current request is the first page.
The data of the server response is as follows. For the convenience of reading, I have simplified the response data:
{
"items": [
{
"id": 31792824,
"name": "flutter",
"description": "Flutter makes it easy and fast to build beautiful apps for mobile and beyond.",
"stargazers_count": 112819,
},
{
"id": 14098069,
"name": "free-programming-books",
"description": ":books",
"stargazers_count": 76056,
},
{
"id": 111583593,
"name": "scrcpy",
"description": "Display and control your Android device",
"stargazers_count": 44713,
},
{
"id": 12256376,
"name": "ionic-framework",
"description": "A powerful cross-platform UI toolkit for building native-quality iOS, Android, and Progressive Web Apps with HTML, CSS, and JavaScript.",
"stargazers_count": 43041,
},
{
"id": 55076063,
"name": "Awesome-Hacking",
"description": "A collection of various awesome lists for hackers, pentesters and security researchers",
"stargazers_count": 42876,
}
]
}
Code language: JSON / JSON with Comments (json)
The simplified data format is still very easy to understand. The items array records which libraries are included on the first page, where name indicates the name of the library, description indicates the description of the library, and stargazers_count indicates the number of stars in the library.
Then let’s write the network-related code according to this interface. this part belongs to the usage of Retrofit, I will introduce it briefly.
- First, define the corresponding entity class according to the Json format of the server response, and create a new Repo class. The code is as follows:
data class Repo(
@SerializedName("id") val id: Int,
@SerializedName("name") val name: String,
@SerializedName("description") val description: String?,
@SerializedName("stargazers_count") val starCount: Int
)
Code language: CSS (css)
- Then define a
RepoResponse
class that wraps theRepo
class in the form of a collection:
class RepoResponse(
@SerializedName("items") val items: List<Repo> = emptyList()
)
Code language: CSS (css)
- Next, define a
GitHubService
to provide a network request interface, as follows:
interface GitHubService {
@GET("search/repositories?sort=stars&q=Android")
suspend fun searchRepos(@Query("page") page: Int, @Query("per_page") perPage: Int): RepoResponse
companion object {
private const val BASE_URL = "https://api.github.com/"
fun create(): GitHubService {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(GitHubService::class.java)
}
}
}
Code language: PHP (php)
These are the standard usages of Retrofit
. Now when the searchRepos()
function is called, Retrofit
will automatically initiate a network request to the GitHub server interface and parse the response data into the RepoResponse
object.
Well, now the network-related code is ready, let’s start using Paging 3 to implement the paging loading function.
Paging 3 has several very critical core components, and we need to implement paging logic step by step in these core components.
The first and most important component is PagingSource
. We need to customize a subclass to inherit PagingSource
, then rewrite the load()
function, and provide the data corresponding to the current number of pages here.
Create a new RepoPagingSource
that inherits from PagingSource
, and the code is as follows:
class RepoPagingSource(private val gitHubService: GitHubService) : PagingSource<Int, Repo>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Repo> {
return try {
val page = params.key ?: 1 // set page 1 as default
val pageSize = params.loadSize
val repoResponse = gitHubService.searchRepos(page, pageSize)
val repoItems = repoResponse.items
val prevKey = if (page > 1) page - 1 else null
val nextKey = if (repoItems.isNotEmpty()) page + 1 else null
LoadResult.Page(repoItems, prevKey, nextKey)
} catch (e: Exception) {
LoadResult.Error(e)
}
}
override fun getRefreshKey(state: PagingState<Int, Repo>): Int? = null
}
Code language: HTML, XML (xml)
This code is not long, but it needs a good explanation.
When inheriting PagingSource
, two generic types need to be declared. The first type represents the data type of the number of pages. We have no special requirements, so we can directly use the integer type. The second type represents the object type corresponding to each item of data (note that it is not each page), and the Repo just defined is used here.
Then in the load()
function, first get the key through the params parameter, which represents the current number of pages. Note that the key may be null. If it is null, we will set the current page number to the first page by default. In addition, you can also get loadSize
through the params parameter, indicating how many pieces of data each page contains. We can set the size of this data later.
Next, call the searchRepos()
interface just defined in GitHubService
, and pass in page
and pageSize
to get the data corresponding to the current page from the server.
Finally, you need to call the LoadResult.Page()
function, build a LoadResult
object and return it. Note that the LoadResult.Page()
function receives 3 parameters. The first parameter is passed in the Repo list parsed from the response data. The second and third parameters correspond to the number of pages on the previous page and the next page, respectively. For the previous page and the next page, we also make an additional judgment. If the current page is already the first or last page, then its previous or next page is null.
In this way, the role of the load()
function has been explained, and you may find that the above code also rewrites a getRefreshKey()
function. This function was added in Paging 3.0.0-beta01, and was not available in previous alpha versions. It belongs to the more advanced usage of Paging 3, which we will not cover in this article, so just return null directly.
After the logic related to PagingSource is written, the next step is to create a Repository class. This is an important component of the MVVM architecture
object Repository {
private const val PAGE_SIZE = 50
private val gitHubService = GitHubService.create()
fun getPagingData(): Flow<PagingData<Repo>> {
return Pager(
config = PagingConfig(PAGE_SIZE),
pagingSourceFactory = { RepoPagingSource(gitHubService) }
).flow
}
}
Code language: PHP (php)
Although this code is very short, it is not easy to understand because the Flow of the coroutine is used. I can’t explain what Flow is here, you can simply understand it as a technology that benchmarks RxJava in coroutines.
Of course, there is no complicated Flow technology used here. As you can see, the above code is very short, which is more of a fixed way of writing than understanding.
We define a getPagingData()
function, the return value of this function is Flow<PagingData<Repo>>
, note that except for the Repo part, which can be changed, other parts are fixed.
In the getPagingData()
function, a Pager object is created here, and .flow
is called to convert it into a Flow object. When creating the Pager object, we specified PAGE_SIZE
, which is the amount of data contained in each page. The pagingSourceFactory
is specified again, and our custom RepoPagingSource
is passed in, so that Paging 3 will use it as the data source for paging.
After the Repository is written, we also need to define a ViewModel, because the Activity cannot directly interact with the Repository, but only with the help of the ViewModel. Create a new MainViewModel class with the following code:
class MainViewModel : ViewModel() { fun getPagingData(): Flow<PagingData<Repo>> { return Repository.getPagingData().cachedIn(viewModelScope) } }
The code is very simple, it just calls the getPagingData()
function defined in the Repository. However, an additional cachedIn()
function is called here, which is used to cache the data returned by the server in the scope of viewModelScope. If the mobile phone is rotated horizontally and vertically, the Activity is re-created, and Paging 3 can directly read the cache. data without having to re-initiate the network request.
At this point, more than half of our project has been completed, and then we will start the work related to interface display.
Since Paging 3 must be used in conjunction with RecyclerView
, let’s define a sub-item layout of RecyclerView. Create a new repo_item.xml, the code is as follows:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
android:orientation="vertical">
<TextView
android:id="@+id/name_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:maxLines="1"
android:ellipsize="end"
android:textColor="#5194fd"
android:textSize="20sp"
android:textStyle="bold" />
<TextView
android:id="@+id/description_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:maxLines="10"
android:ellipsize="end" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:gravity="end"
tools:ignore="UseCompoundDrawables">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginEnd="5dp"
android:src="@drawable/ic_star"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/star_count_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical" />
</LinearLayout>
</LinearLayout>
Code language: HTML, XML (xml)
This layout uses an image resource, which can be obtained from the source code of this project. The source code address is at the bottom of the article.
Next, define the adapter of RecyclerView, but note that this adapter is also special and must inherit from PagingDataAdapter
. The code is as follows:
class RepoAdapter : PagingDataAdapter<Repo, RepoAdapter.ViewHolder>(COMPARATOR) {
companion object {
private val COMPARATOR = object : DiffUtil.ItemCallback<Repo>() {
override fun areItemsTheSame(oldItem: Repo, newItem: Repo): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Repo, newItem: Repo): Boolean {
return oldItem == newItem
}
}
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val name: TextView = itemView.findViewById(R.id.name_text)
val description: TextView = itemView.findViewById(R.id.description_text)
val starCount: TextView = itemView.findViewById(R.id.star_count_text)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.repo_item, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val repo = getItem(position)
if (repo != null) {
holder.name.text = repo.name
holder.description.text = repo.description
holder.starCount.text = repo.starCount.toString()
}
}
}
Code language: HTML, XML (xml)
Compared with a traditional RecyclerView Adapter, the most special place here is to provide a COMPARATOR. This COMPARATOR is necessary because Paging 3 uses DiffUtil internally to manage data changes. If you have used DiffUtil before, this should be familiar.
Besides, we don’t need to pass the data source to the parent class, because the data source is managed internally by Paging 3 itself. At the same time, there is no need to rewrite the getItemCount()
function. The reason is the same. Paging 3 can know how many pieces of data there are.
The other parts are no different from ordinary RecyclerView Adapter, I believe everyone can understand.
The next step is the last step, let’s integrate everything into the Activity.
Modify the activity_main.xml layout and define a RecyclerView and a ProgressBar in it:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
</FrameLayout>
Code language: HTML, XML (xml)
Then modify the code in MainActivity as follows:
class MainActivity : AppCompatActivity() {
private val viewModel by lazy { ViewModelProvider(this).get(MainViewModel::class.java) }
private val repoAdapter = RepoAdapter()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val recyclerView = findViewById<RecyclerView>(R.id.recycler_view)
val progressBar = findViewById<ProgressBar>(R.id.progress_bar)
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.adapter = repoAdapter
lifecycleScope.launch {
viewModel.getPagingData().collect { pagingData ->
repoAdapter.submitData(pagingData)
}
}
repoAdapter.addLoadStateListener {
when (it.refresh) {
is LoadState.NotLoading -> {
progressBar.visibility = View.INVISIBLE
recyclerView.visibility = View.VISIBLE
}
is LoadState.Loading -> {
progressBar.visibility = View.VISIBLE
recyclerView.visibility = View.INVISIBLE
}
is LoadState.Error -> {
val state = it.refresh as LoadState.Error
progressBar.visibility = View.INVISIBLE
Toast.makeText(this, "Load Error: ${state.error.message}", Toast.LENGTH_SHORT).show()
}
}
}
}
}
Code language: HTML, XML (xml)
The most important piece of code here is to call the submitData()
function of the RepoAdapter. This function is the core of triggering the paging function of Paging 3. After calling this function, Paging 3 starts to work.
submitData()
receives a PagingData parameter, which we need to call the collect()
function of the Flow object returned in the ViewModel to get it. The collect()
function is somewhat similar to the subscribe()
function in Rxjava. In short, after subscribing, the message It will keep spreading here.
However, since the collect()
function is a suspending function, it can only be called in the coroutine scope, so the lifecycleScope.launch()
function is called here to start a coroutine.
There should be nothing else to explain in other places, all of which are the usage of some traditional RecyclerView
Ok, so we have completed the whole project. Before running the project, don’t forget to add network permissions to your AndroidManifest.xml
file:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.paging3sample">
<uses-permission android:name="android.permission.INTERNET" />
...
</manifest>
Code language: HTML, XML (xml)
Now run the program, the effect is as shown below:

It can be seen that the Android-related open source libraries on GitHub have been successfully displayed. And you can keep sliding down, and Paging 3 will automatically load more data, as if you will never end.
To sum up, compared to the traditional paging implementation, Paging 3 hides some trivial details. For example, you don’t need to listen to the sliding event of the list, and you don’t need to know when the data on the next page should be loaded. Encapsulated by Paging 3. We only need to write the logic implementation according to the framework built by Paging 3, tell Paging 3 how to load data, and Paging 3 will automatically complete other things for us.
Show loading status at bottom
According to the design of Paging 3, in theory, we should not see the loading state at the bottom. Because Paging 3 will preload more data when the list is far from sliding to the bottom (this is the default property, configurable), resulting in a feeling that it never ends.
However, there are always surprises in everything. For example, the current network speed is not very good. Although Paging 3 will load the data of the next page in advance, when you slide to the bottom of the list, the data responded by the server may not be returned. At this time, you should have a loading status displayed at the bottom.
Also, if the network conditions are very bad, the load may fail, and a retry button should appear at the bottom of the list.
Then we will implement this function next to make the project more perfect.
Create a footer_item.xml layout to display the loading progress bar and retry button:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp">
<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
<Button
android:id="@+id/retry_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="Retry" />
</FrameLayout>
Code language: HTML, XML (xml)
Then create a FooterAdapter
as the bottom adapter of the RecyclerView, note that it must inherit from LoadStateAdapter
, as follows:
class FooterAdapter(val retry: () -> Unit) : LoadStateAdapter<FooterAdapter.ViewHolder>() {
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val progressBar: ProgressBar = itemView.findViewById(R.id.progress_bar)
val retryButton: Button = itemView.findViewById(R.id.retry_button)
}
override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.footer_item, parent, false)
val holder = ViewHolder(view)
holder.retryButton.setOnClickListener {
retry()
}
return holder
}
override fun onBindViewHolder(holder: ViewHolder, loadState: LoadState) {
holder.progressBar.isVisible = loadState is LoadState.Loading
holder.retryButton.isVisible = loadState is LoadState.Error
}
}
Code language: HTML, XML (xml)
This is still a very simple Adapter, and there are only two things to be aware of.
- First, we use Kotlin’s higher-order functions to register the click event for the retry button, so that when the retry button is clicked, the function type parameter passed in the constructor will be called back, and we will add the retry there later. Try logic.
- The second point is that in
onBindViewHolder()
, it will decide how to display the bottom interface according to the state of LoadState. If it is loading, it will display the loading progress bar. If the loading fails, then display the retry button. - Finally, modify the code in MainActivity to integrate
FooterAdapter
intoRepoAdapter
:
class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { ... recyclerView.adapter = repoAdapter.withLoadStateFooter(FooterAdapter { repoAdapter.retry() }) ... } }
The code is very simple, you only need to change one line and call the withLoadStateFooter()
function of the RepoAdapter to integrate the FooterAdapter into the RepoAdapter.
Also note that the Lambda expression is used here as the function type parameter passed to the FooterAdapter. In the Lambda expression, the retry()
function of the RepoAdapter can be called to reload.
In this way, we have completed the function of displaying the loading status at the bottom. Let’s test it now. The effect is shown in the following figure.

As you can see, first I turned on airplane mode on the device so that when I swipe to the bottom of the list, the retry button appears
Then turn off the airplane mode and click the retry button, so that the loading progress bar will be displayed, and the new data will be loaded successfully.