Before I wrote a paging2. x is the use and analysis, the effect of paging2. x running infinite sliding is quite good, but the code is a bit troublesome to write, the function is not too perfect, for example, the method of pull down refresh is not provided, We’ll have to call the DataSource#invalidate() method ourselves to reset the data. Google recently released a beta version of 3.0 that is more powerful and easier to use, so let’s get started

Take a look at the features of Paging3.0 on the official website

  • Paging data is cached in memory to ensure that applications use system resources more efficiently when processing paging data
  • Multiple simultaneous requests trigger only one, ensuring that the App uses network and system resources efficiently
  • You can configure RecyclerView adapters to slide to the end to automatically initiate requests
  • There is good support for Kotlin coroutines and flows, as well as LiveData and RxJava
  • Built-in refresh, retry, error handling and other functions

To get started, the dependency library is introduced first

def paging_version = "3.0.0 - alpha02"
implementation "androidx.paging:paging-runtime:$paging_version"
Copy the code

Configure a RecyclerView, mainly need two parts one is Adapter, one is data. Start with the Adapter

Build the Adapter

Paging2.x extends PagedListAdapter. In 3.0, PagedListAdapter is no longer available, so you need to inherit PagingDataAdapter

class ArticleAdapter : PagingDataAdapter<Article,ArticleViewHolder>(POST_COMPARATOR){

    companion object{
        val POST_COMPARATOR = object : DiffUtil.ItemCallback<Article>() {
            override fun areContentsTheSame(oldItem: Article, newItem: Article): Boolean =
                    oldItem == newItem

            override fun areItemsTheSame(oldItem: Article, newItem: Article): Boolean =
                    oldItem.id == newItem.id
        }
    }

    override fun onBindViewHolder(holder: ArticleViewHolder, position: Int){ holder.tvName.text = getItem(position)? .title }override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ArticleViewHolder {
        return ArticleViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.list_item,parent,false))}}class ArticleViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView){
   val tvName: TextView = itemView.findViewById(R.id.tvname)
}
Copy the code

Write the same as normal recyclerView. Adapter, just add one thing, need to pass a diffUtil. ItemCallback in the constructor to determine the calculation rule of the difference update.

Now that the Adapter is finished, here’s the data. We use Retrofit and The Kotlin coroutine to get the data from the network and set it to the Adapter

Get the data and set it to the Adapter

According to the official website, Google recommended that I use a three-tier architecture to complete the setup of data to Adapter, as shown below on the official website

The first data warehouse layer Repository

The Repository layer uses the PagingSource component, each of which corresponds to a data source and how to find data from that source. PagingSource can look up data from any single data source such as the network or database.

The Repository layer also has another paging component that can use RemoteMediator, which is a hierarchical data source, such as a network data source with a local database cache.

Let’s create our PagingSource and Repository

class ArticleDataSource:PagingSource<Int,Article>() {

    /** * Implement this method to trigger asynchronous loading (e.g. from a database or network). Suspend is a suspend function that can be conveniently loaded asynchronously using coroutines
    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Article> {

        return try {
            valpage = params.key? :0
            // Get network data
            val result = WanRetrofitClient.service.getHomeList(page)
            LoadResult.Page(
                    // Data to load
                    data = result.data.datas,
                    // Set this parameter if more can be loaded, otherwise not
                    prevKey = null.// Add a key to the next page
                    nextKey = if(result.data.curPage==result.data.pageCount) null else page+1)}catch (e:Exception){
            LoadResult.Error(e)
        }

    }
}
Copy the code
  • Inheriting PagingSource requires two generics. The first represents how the data on the next page should be loaded, such as an Int, and the next page should be loaded using an attribute of the last data, and the next page should be loaded using another type, such as String
  • It is a suspend function decorated with suspend. It is easy to use the coroutine to load asynchronously.
  • The LoadParams parameter has a key value that we can pull out to load the next page.
  • The return value is a LoadResult, an exception was calledLoadResult.Error(e), it can be used under normal strong opening conditionLoadResult.PageMethod to set data retrieved from the network or database
  • PrevKey and nextKey represent load factors that need to be provided for the next up or down load, respectively. For example, if we load each page by increasing the number of pages, nextKey can be passed to the next page+1. If set to null, there is no data.

Create the Repository

class ArticleRepository {

    fun getArticleData(a) = Pager(PagingConfig(pageSize = 20)){
        ArticleDataSource()
    }.flow

}
Copy the code

The code is minimal but there are two important objects: Pager and PagingData

  • Pager is the main entry point to paging. It takes four parameters: PagingConfig, Key, RemoteMediator, and PagingSource. The first and fourth parameters are mandatory.
  • PagingConfig is used to configure loading properties, such as how many bars count as a page, how far from the bottom to start the next page, the initial number of bars to load, and so on.
  • PagingData is used to store the results of each pagination
  • A flow is kotlin’s asynchronous data flow, similar to an RxJava Observable

Layer 2 ViewModel layer

Repository ultimately returns an asynchronous stream-wrapped PagingDataFlow > that stores data results and can eventually be used to associate data with a UI

LiveData is commonly used in viewModels to interact with the UI layer, and the Flow extension function can be directly converted into a LiveData observable

class PagingViewModel:ViewModel() {

    private val repository:ArticleRepository by lazy { ArticleRepository() }
    /** * Pager page entry Each PagingData represents a page of data and finally calls asLiveData to convert the result into a listening LiveData */
    fun getArticleData(a) = repository.getArticleData().asLiveData()

}
Copy the code

The UI layer

The UI layer is actually in our Activity, set up Adapter for RecycleView, set up data for Adater

class PagingActivity : AppCompatActivity() {

    private val viewModel by lazy { ViewModelProvider(this).get(PagingViewModel::class.java)}

    private val adapter: ArticleAdapter by lazy { ArticleAdapter() }

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_paging)

        val refreshView:SmartRefreshLayout = findViewById(R.id.refreshView)
        val recyclerView :RecyclerView = findViewById(R.id.recyclerView)
        recyclerView.layoutManager = LinearLayoutManager(this)
        recyclerView.adapter = adapter.withLoadStateFooter(PostsLoadStateAdapter(adapter))
        // Get the data and render the UI
        viewModel.getArticleData().observe(this, Observer {
            lifecycleScope.launchWhenCreated {
                adapter.submitData(it)
            }
        })
        When the refresh is complete, close the refresh
        lifecycleScope.launchWhenCreated {
            @OptIn(ExperimentalCoroutinesApi::class)
            adapter.loadStateFlow.collectLatest {
                if(it.refresh !is LoadState.Loading){
                    refreshView.finishRefresh()
                }
            }
        }
        refreshView.setOnRefreshListener {
            adapter.refresh()
        }
    }
}
Copy the code
  • Create an instance of Adapter and set it to RecyclerView
  • callviewModel.getArticleData()Method to get LiveData and listen for the returned data
  • Call the Adapter submitData method to trigger the rendering of the page. This method is a suspend method decorated by suspend, so call it in a lifetime coroutine. If you don’t want to put it in a coroutine you can call another method with two argumentsadapter.submitData(lifecycle,it)Lifecycle will do

Refresh and retry

In Paging3.0, the method of calling refresh is much more convenient than in Paging2.x. It provides the method of refreshing directly, and also provides the method of retrying after loading data error.

The activity in front of the code, in the drop-down to refresh the control in the drop-down to monitor directly calling adapter. The refresh () method can complete the refresh, what time do you close to refresh the animation, you need to call adapter. LoadStateFlow. CollectLatest method to listen

 lifecycleScope.launchWhenCreated {
            @OptIn(ExperimentalCoroutinesApi::class)
            adapter.loadStateFlow.collectLatest {
                if(it.refresh ! is LoadState.Loading){ refreshView.finishRefresh() } } }Copy the code

Collect the state of the stream. If the Loading state is complete, you can turn off the animation.

PagingDataAdapter can set the header and bottom of the loading progress or load error layout, so that when in the loading state, can display the loading animation, load error can display the retry button. It’s also easy and comfortable to use.

You need to define a custom Adapter that inherits from the LoadStateAdapter and set it to the original Adapter

class PostsLoadStateAdapter(
        private val adapter: ArticleAdapter
) : LoadStateAdapter<NetworkStateItemViewHolder>() {
    override fun onBindViewHolder(holder: NetworkStateItemViewHolder, loadState: LoadState) {
        holder.bindTo(loadState)
    }

    override fun onCreateViewHolder(
            parent: ViewGroup,
            loadState: LoadState
    ): NetworkStateItemViewHolder {
        return NetworkStateItemViewHolder(parent) { adapter.retry() }
    }
}
Copy the code

ViewHolder

class NetworkStateItemViewHolder(
    parent: ViewGroup,
    private val retryCallback: () -> Unit
) : RecyclerView.ViewHolder(
    LayoutInflater.from(parent.context).inflate(R.layout.network_state_item, parent, false)) {private val progressBar = itemView.findViewById<ProgressBar>(R.id.progress_bar)
    private val errorMsg = itemView.findViewById<TextView>(R.id.error_msg)
    private val retry = itemView.findViewById<Button>(R.id.retry_button)
        .also {
            it.setOnClickListener { retryCallback() }
        }

    fun bindTo(loadState: LoadState) {
        progressBar.isVisible = loadState is Loading
        retry.isVisible = loadState isError errorMsg.isVisible = ! (loadStateas? Error)? .error? .message.isNullOrBlank() errorMsg.text = (loadStateas? Error)? .error? .message } }Copy the code

There are three loadstates: NotLoading, Loading, and Error. We can change the layout style of the bottom or top according to different states

Finally, in the activity Settings, add a bottom layout below

recyclerView.adapter = adapter.withLoadStateFooter(PostsLoadStateAdapter(adapter))
Copy the code

You can simply call the relevant adapter methods, add the bottom, add the header, and add both.

The simple use of Paging3.0 to this effect is as follows: