Hello, everyone.
With the official release of Android 11, many new members of the Jetpack family have been introduced. I have promised that I will write an article respectively to introduce the newly introduced App Startup, Hilt and Paging 3.
In this article, we will learn about Paging 3.
The Paging 3 introduction
Paging is a Paging loading library for Android platform developed by Google.
In fact, Paging is not new, it has been released in two versions before.
However, Paging 3 has changed so much from the previous two versions that it is almost completely different. So it does not matter if you have not learned Paging before, just learn Paging 3 as a new library.
I believe that many friends will have the same idea with me when learning Paging 3: it is not difficult to realize the Paging function on Android itself, and we can do it completely even without the Paging library. However, why does Paging 3 design such a complex function that is originally quite simple?
Yes, Paging 3 is complex, at least if you don’t know it yet. I was discouraged from Paging 3 the first time I learned how to do it. I thought, “Why bother with Paging 3?”
Later, with the attitude of embracing new technology, I learned Paging 3 again and basically mastered it this time. Moreover, I applied the technology of Paging 3 in my new open source project Glance.
If I were to evaluate Paging 3 again, I would probably have gone through a process from teasing to really sweet. After understanding Paging 3, you will find that it provides a very reasonable Paging architecture, which can be easily implemented by writing business logic according to the architecture. I hope Paging 3 will also smell good after reading this article.
However, I can’t vouch for the intelligibility of this article. Although many friends think my article is simple and easy to understand, the complexity of Paging 3 lies in the fact that it is associated with too many other knowledge, such as coroutine, Flow, MVVM, RecyclerView, DiffUtil, etc. If you cannot understand all the related knowledge, Mastering Paging 3 will be even more difficult.
In addition, since Paging 3 is a new library rewritten by Google based on Kotlin coroutines, it is mainly used in Kotlin language (Java also works, but will be more complex), and there will be more and more such libraries in the future, such as Jetpack Compose, etc. If you’re not familiar with Kotlin, check out my new book, First Line of Code Android Version 3.
To fit the Paging 3
After my own summary, I find that it is difficult to master Paging 3 if I introduce some knowledge about Paging 3 piecemeal. The best way to learn Paging 3 is to start a project directly. After the project is completed, you will basically master the Paging 3. In this article, we’ll take that approach.
Also, I’m sure you’ve all done this before, but as I said, it’s not that hard to implement. But for now, forget all about the Paging scheme you used to be familiar with, because not only will it not help you understand Paging 3, it will greatly affect your understanding of Paging 3.
Yes, don’t even think about listening for a list slide event. Swipe to the bottom and make a network request to load the next page of data. Paging 3 does not work in this way at all, and learning Paging 3 will be difficult if you retain this old implementation approach.
So let’s get started.
Let’s start with a new Android project, which I’ll call Paging3Sample.
Next, we add the necessary dependency libraries to the build.gradle dependencies:
dependencies { ... Implementation 'androidx. The paging: the paging - runtime: 3.0.0 - beta01' implementation 'com. Squareup. Retrofit2: retrofit: 2.9.0' Implementation 'com. Squareup. Retrofit2: converter - gson: 2.9.0'}Copy the code
Note that although Paging 3 works in conjunction with many other association libraries as I said earlier, it is not necessary to import these libraries manually. Once Paging 3 is introduced, all association libraries will be automatically downloaded.
Retrofit’s library is also introduced here, as we will later request data from the network and display it in Paging 3.
Before using Paging 3, let’s set up the network code to provide Paging data for Paging 3.
Here, I plan to use GitHub’s public API as the data source of our project. Please note that GitHub is generally accessible in China, but sometimes the interface is unstable. If you cannot normally request data, please go online scientifically.
We can try requesting the following interface address in the browser:
https://api.github.com/search/repositories?sort=stars&q=Android&per_page=5&page=1
Copy the code
This interface says it will return all android-related open source libraries on GitHub, sorted by the number of stars, with 5 entries per page, the first page being requested.
The server responds with the following data, which I have simplified for ease of reading:
{ "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-zh_cn ", "description": ": books-zh_cn ", "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,}}]Copy the code
The simplified data format is easy to understand. The items array records which libraries the first page contains, where name represents the library name, description represents the description of the library, and stargazers_count represents the number of stars of the library.
So let’s write some network-related code based on this interface, which I’ll cover briefly because it’s a Retrofit usage.
First, define the corresponding entity class based on the Json format of the server response, and create a new Repo class 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 )Copy the code
Then define a RepoResponse class that wraps the Repo class as a collection:
class RepoResponse(
@SerializedName("items") val items: List<Repo> = emptyList()
)
Copy the code
Next, define a GitHubService to provide the 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) } } }Copy the code
These are standard uses of Retrofit, which now automatically makes a network request to GitHub’s server interface when we call searchRepos() and parses the response data into a RepoResponse object.
Now that the network code is in place, we will use Paging 3 to implement Paging loading.
Paging 3 has several key core components in which you need to implement Paging logic step by step.
The first and most important component is PagingSource. We need to customize a subclass to inherit PagingSource, and then override the load() function to provide data for the current page number.
Create a new RepoPagingSource that inherits from PagingSource and looks like this:
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 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 }Copy the code
This code isn’t long, but it needs a lot of explanation.
We need to declare two generic types when inheriting PagingSource. The first type represents the data type of the page number. We have no special requirements, so we can just use an integer. The second type represents the object type for each item of data (note not each page), using the Repo I just defined.
Then in the load() function, the params parameter is used to get the key, which represents the current page number. Note that key can be null; if it is, we default the current page count to the first page. You can also get loadSize via the params parameter, which indicates how many pieces of data each page contains. We can set the size of this data later.
We then call the searchRepos() interface we just defined in GitHubService and pass in page and pageSize to get the data for the current page from the server.
Finally, the loadResult.page () function is called to build a LoadResult object and return it. Note that the loadresult.page () function takes three arguments, the first passing in a list of repOs parsed from the response data, and the second and third arguments corresponding to the number of pages on the previous and next pages, respectively. For previous and next pages, we also make an additional judgment that if the current page is already the first or last page, then its previous or next page is null.
Now that the load() function is explained, you may notice that the code overrides a getRefreshKey() function. This function is new to Paging 3.0.0-beta01 and was not available in previous alpha releases. It is an advanced use of Paging 3 and will not be covered in this article, so simply return NULL.
With the PagingSource logic written, you need to create a Repository class. This is an important component of the MVVM architecture. For those who are not familiar with it, please refer to chapter 15 of the first Line of code Android Version 3.
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
}
}
Copy the code
This code is short, but not easy to understand because it uses coroutine Flow. I can’t expand here to explain what Flow is, but you can simply think of it as a technique in coroutines that is benchmarking RxJava.
Of course, there is no complicated Flow technique here, as you can see, the above code is very short, and more of a formality than an understanding.
We define a getPagingData() function that returns Flow
In the getPagingData() function, a Pager object is created and.flow is called to convert it to a flow object. When we created the Pager object, we specified PAGE_SIZE, which is the amount of data per page. PagingSourceFactory is specified and our custom RepoPagingSource is passed in so that Paging 3 will use it as the data source for Paging.
Once Repository is written, we need to define a ViewModel because activities can’t interact with Repository directly. Create a new MainViewModel class as follows:
class MainViewModel : ViewModel() {
fun getPagingData(): Flow<PagingData<Repo>> {
return Repository.getPagingData().cachedIn(viewModelScope)
}
}
Copy the code
The code is simple: it simply calls the getPagingData() function defined in Repository. In addition, a cachedIn() function is called, which is used to cache data returned by the server within the viewModelScope. If the phone rotates in horizontal and vertical directions causing the Activity to be created, Paging 3 can read the cached data directly. Instead of having to re-initiate the network request.
At this point, we have completed most of the project, and now we start to do the work related to the interface display.
Since Paging 3 must be used in combination with RecyclerView, we define a RecyclerView subitem layout below. Create a new rePO_item.xml with the following code:
<? The 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>Copy the code
This layout uses an image resource, which can be obtained from the source code of this project, at the bottom of the article.
Then we define the adapter of RecyclerView, but note that this adapter is also special and must inherit from PagingDataAdapter 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() } } }Copy the code
Compared to a traditional RecyclerView Adapter, the most special thing is to provide a COMPARATOR. Because Paging 3 uses DiffUtil internally to manage data changes, this COMPARATOR is required. If you have used DiffUtil before, this should be familiar.
In addition, we do not need to pass the data source to the parent class because the data source is managed internally by Paging 3 itself. There is also no need to rewrite the getItemCount() function for the same reason that Paging 3 will know how many pieces of data there are.
Other parts and ordinary RecyclerView Adapter no different, I believe we can see it.
Now for the final step, let’s integrate everything into the Activity.
Modify the activity_main. XML layout to define a RecyclerView and a ProgressBar:
<? The 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>Copy the code
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()
}
}
}
}
}
Copy the code
The most important piece of code here is the call to the submitData() function of the RepoAdapter. This function is the core that triggers Paging 3, and Paging 3 becomes operational after this function is called.
SubmitData () receives a PagingData argument that we need to call collect() on the Flow object returned in ViewModel, Collect () is a bit like the SUBSCRIBE () function in Rxjava, which simply means that once you subscribe, messages will be sent to it.
But since the collect() function is a hang function that can only be called in the coroutine scope, the lifecyclescopes.launch () function is called to launch a coroutine.
Other places should have no need to explain, are some traditional RecyclerView usage, I believe we can understand.
Ok, so we’re done with the project. Don’t forget to add network permissions to your Androidmanifest.xml file before running the project:
<? The XML version = "1.0" encoding = "utf-8"? > <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.paging3sample"> <uses-permission android: /> ... </manifest>Copy the code
Now run the program and it will look like the picture below:
As you can see, the Android-related open source library on GitHub has been successfully displayed. And you can keep sliding down, as Paging 3 automatically loads more data, as if you were forever sliding down.
In this case, Paging loading using Paging 3 is successful.
To summarize, Paging 3 hides some trivial details, such as the fact that you do not need to listen for a list slide event or know when to load the next page of data, compared to traditional Paging implementations. These are encapsulated in Paging 3. We only need to write logical implementation according to the framework built by Paging 3 and tell Paging 3 how to load data, and Paging 3 will do other things for us automatically.
Load status is displayed at the bottom
According to the design of Paging 3, the loading state should not theoretically be seen at the bottom. Because Paging 3 loads more data ahead of time (which is the default and configurable) long before the list slides to the bottom, it creates a feeling that it will never slide to the end.
However, there are always accidents in everything, for example, the current network speed is not good. Although Paging 3 will load the data of the next page in advance, when sliding to the bottom of the list, the data that the server responds to may not be returned. In this case, a loading status should be displayed at the bottom.
Also, if network conditions are very bad, loading may fail, and a retry button should appear at the bottom of the list.
So let’s implement this functionality to make the project better.
Create a footer_item.xml layout that displays the load progress bar and retry button:
<? The 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>Copy the code
Then create a FooterAdapter as the bottom adapter of RecyclerView, note that it must inherit from LoadStateAdapter, as shown below:
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
}
}
Copy the code
This is still a very simple Adapter, and there are probably only two things to note.
First, we use Kotlin’s higher-order function to register the retry button click event, so that when the retry button is clicked, the function type parameter passed in the constructor is called back, where we will add retry logic later.
Second, onBindViewHolder() determines how to display the bottom interface based on the state of LoadState. The loading progress bar is displayed if the interface is loading, and the retry button is displayed if the interface fails.
Finally, modify the code in the MainActivity to integrate the FooterAdapter into the RepoAdapter:
class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) {... recyclerView.adapter = repoAdapter.withLoadStateFooter(FooterAdapter { repoAdapter.retry() }) ... }}Copy the code
The code is very simple. With one line change, the FooterAdapter can be integrated into the RepoAdapter by calling withLoadStateFooter().
Also note that Lambda expressions are used as function type arguments passed to the FooterAdapter. In the Lambda representation, the RepoAdapter’s retry() function is reloaded.
Now that we’ve finished displaying the loading status at the bottom, let’s test it out and see what it looks like below.
As you can see, first I turned on airplane mode on the device so that when I slid to the bottom of the list the retry button was displayed.
Then turn off airplane mode and click the Retry button. The loading progress bar will appear and the new data will be successfully loaded.
The last
That’s the end of this article.
I have to say that the knowledge I have covered in this article is still basic usage of Paging 3, and there are many advanced uses that are not covered in this article. Of course, these basic usages are also the most common, so if you are not planning to become a Paging 3 master, these should be sufficient for day-to-day development work.
If you want to further learn Paging 3, you can refer to the official Google Codelab project at the following address:
Developer.android.com/codelabs/an…
The Paging3Sample project we wrote together just now is actually evolved from Google’s official Codelab project. I rewrote this project and simplified it to some extent according to my own understanding. Go straight to the original project and you’ll learn more.
Finally, if you need to obtain the source code for the Paging3Sample project, please visit the following address:
Github.com/guolindev/P…
Also, if you want to learn about Kotlin and the latest on Android, check out my new book, Line 1, Version 3, [click here for details]
Pay attention to my technical public account “Guo Lin”, there are high-quality technical articles pushed every working day.