More than technology, the article has material, pay attention to the public number nine heart said, a week of high quality good article, and nine heart side by side in dailang road.
preface
It’s time to learn About Android Jetpack. I’ve already written about Android Jetpack-Paging in A few minutes, but Android Jetpack has been updated all the time. The Paging 2 has been upgraded to the Paging 3, then, you may have a lot of attention to the problems, such as:
- What exactly does Paging 3 upgrade?
- How do I use the latest Paging 3?
That’s what I want to talk to you about in this issue. Final results of this issue:
If you want to learn about Android Jetpack, check out my previous post learning About Android Jetpack? This is the eighth installment in the Learn To Use Android Jetpack series
[Hoo] [Hoo]
directory
What is the difference between Paging 2 and Paging 3?
If you have not used Paging 2, you can skip this section.
If you’ve ever used Paging 2, you’ll notice that Paging 3 is a radical change in the way many of the apis are used, but just a few of the main things that have changed:
- Support for Flow in Kotlin.
- Simplified data source
PagingSource
The implementation of the. - Added state callback when requesting data and support for setting headers and footers.
- Supports multiple ways to request data, such as network requests and database requests.
Second, the introduction
Paging 3 Google LABS: official tutorial Official Demo: Github repository query Demo
Definition 1.
Look at the official definition:
The Paging library helps you load and display pages of data from a larger dataset from local storage or over network.
Is to help you solve the problem of displaying large data sets containing local databases and networks in a paging manner.
2. The advantages
Advantages of Paging 3:
- Use memory to help you cache data.
- Built-in request deduplication to help you display data more efficiently.
- When a request is automatically initiated
RecyclerView
When you slide to the bottom. - Support in Kotlin
coroutines
和Flow
, as well asLiveData
和RxJava2
. - Built-in state handling, including refresh, error, load, and so on.
3. Several important classes
First look at the structure:
There are several classes in it:
PagingSource
: a single data source.RemoteMediator
: in fact,RemoteMediator
It’s also a single data source, and it’s going to be inPagingSource
When the data is unavailable, use it againRemoteMediator
The data provided, if there is a database request and a network request, is usuallyPagingSource
Used to make database requests,RemoteMediator
Make a network request.PagingData
: A container for single paging data.Pager
: Used to buildFlow<PagingData>
Class to achieve the completion of the data loading callback.PagingDataAdapter
: for loading data in pagesRecyclerView
Of the adapter.
The page source and RemoteMediator act as data sources. The ViewModel listens for data refreshes using Flow
Third, in actual combat
For simplicity, start with a single data source.
The first step is to introduce dependencies
Def paging_version = "3.0.0-alpha08" implementation "androidx. Paging :paging- Runtime :$paging_version" // Paging :paging-common:$paging_version" // Optional: RxJava implementation support "androidx.paging:paging-rxjava2:$paging_version" // ... Other configurations are not important. For details, see the official document.Copy the code
Step 2 Configure the data source
In Paging 2, there are three Page sources. Developers need to think about the corresponding scenarios when using Paging 2.
In Paging 3, if you have a single data source, you simply inherit the PagingSource.
private const val SHOE_START_INDEX = 0;
class CustomPageDataSource(private val shoeRepository: ShoeRepository) : PagingSource<Int, Shoe>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Shoe> {
valpos = params.key ? : SHOE_START_INDEXval startIndex = pos * params.loadSize + 1
val endIndex = (pos + 1) * params.loadSize
return try {
// Retrieve data from the database
val shoes = shoeRepository.getPageShoes(startIndex.toLong(), endIndex.toLong())
// Return the result of your paging, and fill in the key of the previous page and the key of the next page
LoadResult.Page(
shoes,
if (pos <= SHOE_START_INDEX) null else pos - 1.if (shoes.isNullOrEmpty()) null else pos + 1)}catch (e:Exception){
LoadResult.Error(e)
}
}
}
Copy the code
Normally loadResult.page () is used to return the result. In the case of Error, loadResult.error is used to return the Error.
The third step is to generate observable data sets
The Observable datasets here include LiveData, Flow, and Observable and Flowable in RxJava, which need to be supported by a separate extension library introduced in RxJava.
Shoes uses Flow for the observable data set:
class ShoeModel constructor(private val shoeRepository: ShoeRepository) : ViewModel() {
/ * * *@paramParameter * for the config page@paramPagingSourceFactory The factory of a single data source, providing a PageSource in the closure *@paramRemoteMediator supports data sources for both network and database requests *@paramInitialKey Key */ used for initialization
var shoes = Pager(config = PagingConfig(
pageSize = 20
, enablePlaceholders = false
, initialLoadSize = 20
), pagingSourceFactory = { CustomPageDataSource(shoeRepository) }).flow
/ /... omit
}
Copy the code
As you can see, shoes: Flow
PagerConfig
You can provide paging parameters, such as the number of loads per page, the initial number of loads, and the maximum number.pagingSourceFactory
和remoteMediator
Both are data sources, so we can use one of them.
Step 4 Create Adapter
And ordinary Adapter is not very different, mainly:
- inheritance
PagingDataAdapter
- provide
DiffUtil.ItemCallback<Shoe>
Actual code:
/** * Shoe adaptor with Data Binding use */
class ShoeAdapter constructor(val context: Context) :
PagingDataAdapter<Shoe, ShoeAdapter.ViewHolder>(ShoeDiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
ShoeRecyclerItemBinding.inflate(
LayoutInflater.from(parent.context)
, parent
, false))}override fun onBindViewHolder(holder: ViewHolder, position: Int) {
valshoe = getItem(position) holder.apply { bind(onCreateListener(shoe!! .id), shoe) itemView.tag = shoe } }/** * Holder click event */
private fun onCreateListener(id: Long): View.OnClickListener {
return View.OnClickListener {
val intent = Intent(context, DetailActivity::class.java)
intent.putExtra(BaseConstant.DETAIL_SHOE_ID, id)
context.startActivity(intent)
}
}
class ViewHolder(private val binding: ShoeRecyclerItemBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(listener: View.OnClickListener, item: Shoe) {
binding.apply {
this.listener = listener
this.shoe = item
executePendingBindings()
}
}
}
}
class ShoeDiffCallback: DiffUtil.ItemCallback<Shoe>() {
override fun areItemsTheSame(oldItem: Shoe, newItem: Shoe): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Shoe, newItem: Shoe): Boolean {
return oldItem == newItem
}
}
Copy the code
The fifth step is to use the UI
Let’s start with a simpler one. If we only showed data, we would do this:
- Create and set up the adapter.
- Start a coroutine
- Receive in a coroutine
Flow
Data provided.
My code:
private fun onSubscribeUi(binding: {binding. LifecycleOwner = this // Initialize the RecyclerView section val Adapter = ShoeAdapter(context!!) binding.recyclerView.adapter = adapter job = viewModel.viewModelScope.launch(Dispatchers.IO) { viewModel.shoes.collect() { adapter.submitData(it) } } // ... Omit}Copy the code
Step 6 Set the Header and Footer
Obviously Paging 3 does more than that, right! It also supports adding headers and footers, which the official example uses as a pull-up refresh and pull-down load more controls.
Header and Footer are not used in Hoo, so let’s see how they are used:
- To create a
HeaderAdapter or FooterAdapter
Inherited fromLoadStateAdapter
, with the averageAdapter
The difference is that it’s inonBindViewHolder
MethodLoadState
Parameter, which can provide the currentPaging
是 Loading,NotLoading 和 ErrorIn the state. - With the average
Adapter
Create what you needViewHolder
. - Modify the Settings in step 5 slightly
ShoeAdapter
, to call itwithLoadStateHeaderAndFooter
Methods the bindingHeader
和Footer
For the adapter:
private fun onSubscribeUi(binding: ShoeFragmentBinding) {
binding.lifecycleOwner = this
// Initialize the RecyclerView section
valadapter = ShoeAdapter(context!!) .withLoadStateHeaderAndFooter( header = ReposLoadStateAdapter { adapter.retry() }, footer = ReposLoadStateAdapter { adapter.retry() } ) binding.recyclerView.adapter = adapter/ /... omit
}
Copy the code
Just to be clear, the pseudo code I’m using here. For a detailed tutorial, see this step tutorial on the official website.
Step 7 Listen for data loading status
In addition to adding headers and footers, the PagingDataAdapter can also listen for the loading state of the data. This state is corresponding to the class LoadState, which has three states:
Loading
: Data is being loaded.NotLoading
: If the memory has obtained data, Paging does not need to request more data even if the system goes down.Error
: An error was returned while requesting data.
Code to listen for data status:
adapter.addLoadStateListener {state:CombinedLoadStates->
/ /... State monitoring
}
Copy the code
It’s not a LoadState. It’s a CombinedLoadStates. It’s a CombinedLoadStates.
refresh:LoadState
: State when refreshed, as can be calledPagingDataAdapter#refresh()
Method to refresh data.append:LoadState
: can be interpreted asRecyclerView
The request state of the data when descending.prepend:LoadState
: can be interpreted asRecyclerView
The request status of the data during the upward slide.source
和mediator
Contains the attributes of 123 above,source
Represents a single data source,mediator
Represents a scenario with multiple data sources,source
和mediator
A choice.
With that said, the way I played it, I used a third party refresh control, SmartRefreshLayout, which meant I had to handle the loading state of SmartRefreshLayout myself. SmartRefreshLayout + RecyclerView + FloatingActionButton
Results (Gif has a little problem) :
The code to use is:
private fun onSubscribeUi(binding: ShoeFragmentBinding) {
binding.lifecycleOwner = this
// Initialize the RecyclerView section
val adapter = ShoeAdapter(context!!)
// A callback to the data loading state
adapter.addLoadStateListener { state:CombinedLoadStates ->
currentStates = state.source
// If AppEnd is not in the loaded state but refreshLayout is in the loaded state, refreshLayout stops loading
if (state.append is LoadState.NotLoading && binding.refreshLayout.isLoading) {
refreshLayout.finishLoadMore()
}
// If refresh is not in the loaded state, but refreshLayout is in the refreshed state, refreshLayout stops refreshing
if (state.source.refresh is LoadState.NotLoading && binding.refreshLayout.isRefreshing) {
refreshLayout.finishRefresh()
}
}
binding.recyclerView.adapter = adapter
binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val lastPos = getLastVisiblePosition(binding.recyclerView)
if(! (lastPos == adapter.itemCount -1&& currentStates? .appendis LoadState.Loading)) {
binding.refreshLayout.finishLoadMore()
}
}
})
job = viewModel.viewModelScope.launch(Dispatchers.IO) {
viewModel.shoes.collect {
adapter.submitData(it)
}
}
binding.refreshLayout.setRefreshHeader(DropBoxHeader(context))
binding.refreshLayout.setRefreshFooter(ClassicsFooter(context))
binding.refreshLayout.setOnLoadMoreListener {
// If all the current data has been loaded, no more loading
if(currentStates? .append? .endOfPaginationReached ==true)
binding.refreshLayout.finishLoadMoreWithNoMoreData()
}
/ /... Omit irrelevant code
}
Copy the code
At this point, the normal process runs its course.
Four or more
1. Use RemoteMediator
If PageSource is null, you can use the following:
PageSource
: Makes a database request to provide data.RemoteMediator
: Make network requests, and then store the data that succeeds in the request in the database.
Since using RemoteMediator is essentially the same as using a regular method, I won’t go into more detail here, but you can check out the documentation if you’re interested.
2. Talk about some of the pits I encountered
Before clicking on the brand button, Paing 2 corresponds to the version using LiveData, this is how I wrote it before:
class ShoeModel constructor(shoeRepository: ShoeRepository) : ViewModel() {
// All brands are observed by default
private val brand = MutableLiveData<String>().apply {
value = ALL
}
// Shoe set observation class
val shoes: LiveData<PagedList<Shoe>> = brand.switchMap {
// Room database query, as long as you know that LiveData
> is returned
if (it == ALL) {
// LivePagedListBuilder<Int,Shoe>( shoeRepository.getAllShoes(),PagedList.Config.Builder()
LivePagedListBuilder<Int, Shoe>(
CustomPageDataSourceFactory(shoeRepository) // DataSourceFactory
, PagedList.Config.Builder()
.setPageSize(10) // Number of page loads
.setEnablePlaceholders(false) // Whether to use PlaceHolder display when item is null
.setInitialLoadSizeHint(10) // The number of preloads
.build()
)
.build()
//shoeRepository.getAllShoes()
} else {
val array: Array<String> =
when (it) {
NIKE -> arrayOf("Nike"."Air Jordan")
ADIDAS -> arrayOf("Adidas")
else -> arrayOf(
"Converse"."UA"
, "ANTA"
)
}
shoeRepository.getShoesByBrand(array)
.createPagerList(6.6)}}fun setBrand(brand: String) {
this.brand.value = brand
}
companion object {
public const val ALL = "All"
public const val NIKE = "Nike"
public const val ADIDAS = "Adidas"
public const val OTHER = "other"}}Copy the code
Paging 3: Paging 3
First move the observable data set from Flow to LiveData, i.e. from Pager(…) . The flow to Pager (…). .livedata:
class ShoeModel constructor(private val shoeRepository: ShoeRepository) : ViewModel() {
/ * * *@paramParameter * for the config page@paramPagingSourceFactory The factory of a single data source, providing a PageSource in the closure *@paramRemoteMediator supports data sources for both network and database requests *@paramInitialKey Key */ used for initialization
var shoes = Pager(config = PagingConfig(
pageSize = 20
, enablePlaceholders = false
, initialLoadSize = 20
), pagingSourceFactory = { CustomPageDataSource(shoeRepository) }).liveData
//....
}
Copy the code
Next, I’m listening for data on the ShoeFragment:
job = viewModel.viewModelScope.launch() {
viewModel.shoes.observe(viewLifecycleOwner, Observer<PagingData<Shoe>> {
adapter.submitData(viewLifecycleOwner.lifecycle,it)
})
}
Copy the code
The first pit appears. In Paging 2, PagingSouce retries data from the child thread, while in Paging 3, Paging data from the current thread, there will be no thread switch. If you use database queries directly, you will get an error because Room specifies that database queries cannot be on the main thread.
Well, since the main thread can’t request the database, I’ll put it on the IO thread and add Dispatchers.IO to the launch method:
job = viewModel.viewModelScope.launch(Dispatchers.IO) {
viewModel.shoes.observe(viewLifecycleOwner, Observer<PagingData<Shoe>> {
adapter.submitData(viewLifecycleOwner.lifecycle,it)
})
}
Copy the code
Second pit arrangement, LiveData#observe(…) Methods cannot occur in background threads.
When the data source changes, the data needs to be monitored again, and the code is not as elegant as before:
class ShoeModel constructor(private val shoeRepository: ShoeRepository) : ViewModel() {
var shoes = Pager(config = PagingConfig(
pageSize = 20
, enablePlaceholders = false
, initialLoadSize = 20
), pagingSourceFactory = { CustomPageDataSource(shoeRepository) }).flow
fun setBrand(br: String) {
if (br == ALL) {
shoes = Pager(config = PagingConfig(
pageSize = 20
, enablePlaceholders = false
, initialLoadSize = 20
), pagingSourceFactory = { CustomPageDataSource(shoeRepository) }).flow
} else {
val array: Array<String> =
when (br) {
NIKE -> arrayOf("Nike"."Air Jordan")
ADIDAS -> arrayOf("Adidas")
else -> arrayOf(
"Converse"."UA"
, "ANTA"
)
}
shoes = shoeRepository.getShoesByBrand(array).createPager(20.20).flow
}
}
//....
}
class ShoeFragment : Fragment() {
/ /... omit
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup? , savedInstanceState:Bundle?).: View? {
/ /... omit
onSubscribeUi(binding)
return binding.root
}
/** * Shoe data update notice */
private fun onSubscribeUi(binding: ShoeFragmentBinding) {
/ /... omit
mNike.setOnClickListener {
viewModel.setBrand(ShoeModel.NIKE)
reInitSubscribe(adapter)
shoeAnimation()
}
mAdi.setOnClickListener {
viewModel.setBrand(ShoeModel.ADIDAS)
reInitSubscribe(adapter)
shoeAnimation()
}
mOther.setOnClickListener {
viewModel.setBrand(ShoeModel.OTHER)
reInitSubscribe(adapter)
shoeAnimation()
}
}
private fun reInitSubscribe(adapter: ShoeAdapter){ job? .cancel() job = viewModel.viewModelScope.launch(Dispatchers.IO) { viewModel.shoes.collect() { adapter.submitData(it) } } }private fun setViewVisible(isShow: Boolean) {
nikeGroup.visibility = if (isShow) View.VISIBLE else View.GONE
adiGroup.visibility = if (isShow) View.VISIBLE else View.GONE
otherGroup.visibility = if (isShow) View.VISIBLE else View.GONE
}
}
Copy the code
During this period, I also tried the operator of converting LiveData to Flow, but it didn’t work either. I may not have studied deeply, but students who have an understanding of it can discuss it
Five, the summary
In contrast to Paging 2, Paging 3 is indeed more robust and worthy of use.
In a future article, I’ll look at the source code for Paging 3. Of course, Paging 3 is still in the Alpha phase and is not suitable for our production environment, so I’m going to talk about how to better use Paging 2.