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:

  1. Support for Flow in Kotlin.
  2. Simplified data sourcePagingSourceThe implementation of the.
  3. Added state callback when requesting data and support for setting headers and footers.
  4. 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 initiatedRecyclerViewWhen you slide to the bottom.
  • Support in KotlincoroutinesFlow, as well asLiveDataRxJava2.
  • 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,RemoteMediatorIt’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 pagesRecyclerViewOf the adapter.

The page source and RemoteMediator act as data sources. The ViewModel listens for data refreshes using Flow provided in Pager. Every time RecyclerView is about to roll to the bottom of the time, there will be new data arrival, finally, PagingAdapter complex display data.

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 > is provided by Pager().flow. The parameters in Pager are slightly explained:

  • PagerConfigYou 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:

  • inheritancePagingDataAdapter
  • provideDiffUtil.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:

  1. Create and set up the adapter.
  2. Start a coroutine
  3. Receive in a coroutineFlowData 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:

  1. To create aHeaderAdapter or FooterAdapterInherited fromLoadStateAdapter , with the averageAdapterThe difference is that it’s inonBindViewHolder MethodLoadStateParameter, which can provide the currentPagingLoading,NotLoadingErrorIn the state.
  2. With the averageAdapterCreate what you needViewHolder.
  3. Modify the Settings in step 5 slightlyShoeAdapter, to call itwithLoadStateHeaderAndFooterMethods the bindingHeaderFooterFor 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:

  1. Loading: Data is being loaded.
  2. NotLoading: If the memory has obtained data, Paging does not need to request more data even if the system goes down.
  3. 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.

  1. refresh:LoadState: State when refreshed, as can be calledPagingDataAdapter#refresh()Method to refresh data.
  2. append:LoadState: can be interpreted asRecyclerViewThe request state of the data when descending.
  3. prepend:LoadState: can be interpreted asRecyclerViewThe request status of the data during the upward slide.
  4. sourcemediatorContains the attributes of 123 above,sourceRepresents a single data source,mediatorRepresents a scenario with multiple data sources,sourcemediatorA 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.