preface

Android’s paging3 alpha has been around for a long time. So far alpha7; Share experiences and pits used in the project; Do not speak of principle and source code, pure use experience to share! (Don’t ask me why I used alpha in the project, ask me capricious, ask me paging2 is too hard to use)

The preparatory work

1. Rely on:

Date of writing: 2020-10-21; The latest version is 3.0.0-Alpha07

// Java implementation 'Androidx. paging: Paging - Runtime :3.0.0-alpha07' //kotlin implementation 'androidx. The paging: the paging - runtime - KTX: 3.0.0 - alpha07'Copy the code

I can choose one of two languages, I use Kotlin;

Use:

1.adapter

To use Paging3, the Adapter of RecyclerView must inherit PagingDataAdapter because the subsequent pagination UI and operations are managed by Adapter;

The adpater constructor must pass the diffUtil.itemCallback argument; Those of you who have used AsyncListDiffer should know what it does; Android AsyncListDiffer-RecyclerView’s best friend

DiffUtil.itemCallback

The diffUtil. ItemCallback replaces notifyDataSetChanged to flush the list roughly; After all, rough flushing costs performance;

Three methods are mainly introduced:

override fun areItemsTheSame(oldItem: T, newItem: T): Boolean {}

override fun areContentsTheSame(oldItem: T, newItem: T): Boolean {}

override fun getChangePayload(oldItem: T, newItem: T): Any? {}
Copy the code

Paging3 is designed in such a way that it is not recommended to modify the list data directly; Instead, you operate on the data source, and changes to the data source are automatically updated to the list; The DiffUtil.ItemCallback is used to compare data changes and decide to update the corresponding UI; And perform item animation;

  • areItemsTheSame

Check whether the old and new entries are the same; Generally, the unique identifier ID of the entry can be compared, and the USER interface may not be updated if the entry is different.

  • areContentsTheSame

When the above method determines that it is the same item, it compares the contents of the item to whether they are the same, and updates the UI of the item if they are not. It is recommended to write all the data displayed by the UI in this comparison. If the data is missed, the UI will not update the corresponding fields.

  • GetChangePayload (optional)

This method corresponds to the third argument of the Adapter of RcyclerView; For local refreshes within entries;

override fun onBindViewHolder(
        holder: RecyclerView.ViewHolder,
        position: Int,
        payloads: MutableList<Any>
    )
Copy the code

2. Data request processing

Here use zhihu daily’s interface as an example: database cache remoteMediator without paging3; Because the parameter is annotated as @ OptIn (ExperimentalPagingApi: : class) is still in testing; Here is a pure network request paging scheme; In the actual project, it is impossible to do database cache for every list interface, the workload is too large;

The Paging3 data request uses three classes:

  1. Pager
  2. PagingConfig
  3. PagingSource
  • The main entry to Pager paging data, which is constructed here:
class Pager<Key : Any, Value : Any>
@JvmOverloads constructor(
    config: PagingConfig,
    initialKey: Key? = null,
    @OptIn(ExperimentalPagingApi::class)
    remoteMediator: RemoteMediator<Key, Value>? = null,
    pagingSourceFactory: () -> PagingSource<Key, Value>
)
Copy the code

Its generic Key -> page flag, like a page number, or some other parameter that tells the back end what page I want; Value -> The single data type of the list data, that is, the type of each entry;

InitialKey: indicates the page number of the initial page. (Optional) remoteMediator: indicates the remote data demodulator. PagingSourceFactory: data source factory (a new data source is produced every time data is refreshed)

  • PagingConfig introduction

The first parameter of Pager: config: PagingConfig page logic: Settings such as how many pages per page; Structure:

class PagingConfig @JvmOverloads constructor(
	val pageSize: Int,
	@IntRange(from = 0)
    val prefetchDistance: Int = pageSize,
	val enablePlaceholders: Boolean = true,
	@IntRange(from = 1)
    val initialLoadSize: Int = pageSize*DEFAULT_INITIAL_PAGE_MULTIPLIER,
	val maxSize: Int = MAX_SIZE_UNBOUNDED,
	val jumpThreshold: Int = COUNT_UNDEFINED
)
Copy the code

Parameter Description: pageSize: Indicates the number of items on a page. Placeholder Mandatory prefetchDistance: how far to pre-load the next page, slide to the last item to load the next page, seamless loading (optional) Default is pageSize Enableplaceholder: Whether to enable item placeholder when the total number of items is determined; Lists show all items at once, but have no data; When binding data into the Adapter onBindViewHolder, if the data is empty, the corresponding placeholder item is displayed. Optional. This function is enabled by default. InitialLoadSize: number of entries loaded on the first page, optional. The default value is 3*pageSize (sometimes multiple points of data on the first page are required) maxSize: defines the maximum number of lists; This parameter is optional. The default value is int. MAX_VALUE jumpThreshold specifies the loading failure threshold caused by scrolling a large distance. Optional, default: int.min_value (disabling this feature)

  • PagingSource Paging data source

PagingSourceFactory Products produced by the factory;

abstract class PagingSource<Key : Any, Value : Any> {
	abstract suspend fun load(params: LoadParams<Key>): LoadResult<Key, Value>
}
Copy the code

Generics, like Pager generics, have only one main method to implement: they are infinitely more convenient than Paging2

Parameter description: params: required parameters for the request list Returned value: LoadResult: Result of the request for list data, including the key to be requested for the next page

Examples of usage:

val allNews = Pager(PagingConfig(20), initialKey = initialKey) { object : PagingSource<Long, News.StoriesBean>() { override suspend fun load(params: LoadParams<Long>): LoadResult<Long, News.StoriesBean> { val date = params.key ? : InitialKey return try {val data = api.getNews(date).await() // Network request data.page (data.stories, null, data.date.toLong()) } catch (e: Exception) { LoadResult.Error(e) } } } } .flow .cachedIn(viewModelScope) .asLiveData(viewModelScope.coroutineContext)Copy the code

LoadResult. Page:

constructor( data: List<Value>, prevKey: Key? , nextKey: Key? )Copy the code

PrevKey: The key of the previous page (null indicates no previous page) nextKey: the key of the next page (null indicates no next page)

Paging3 uses flow to pass data. If you don’t know, you can search flow. CachedIn binding coroutine life cycle, which must be added or may crash; AsLiveData anyone familiar with LiveData knows how to use it;

Bind data to the Adapter

model.allNews.observe(this@ZhiHuActivity, Observer {
            lifecycleScope.launchWhenCreated {
                adapter.submitData(it)
            }
        })
Copy the code

Adapter. submitData is a coroutine suspend operation, so coroutine assignment is put in; LifecycleScope. LaunchWhenCreated and viewModelScope; Life cycle assistance that depends on coroutines is required as follows:

/ / life cycle auxiliary KTX implementation 'androidx. Lifecycle: lifecycle - viewmodel - KTX: 2.3.0 - beta01' implementation 'androidx. Lifecycle: lifecycle - runtime - KTX: 2.3.0 - beta01' implementation 'androidx. Lifecycle: lifecycle - livedata - KTX: 2.3.0 - beta01'Copy the code

3.UI state processing and operation

The drop-down refresh

The first request does not require any action, subscription data request directly; Manually pull down refresh directly called:

adapter.refresh()
Copy the code

It’s that simple, much more convenient than Paging2

Pull on loading

Paging3 loads seamlessly, there’s actually no manual pull-up but if the user slides too fast it still shows the pull-up UI, and there’s UI processing logic underneath

Failure to retry

adapter.retry()
Copy the code

Mainly used to load more retries.

UI state handling

Adapter. AddLoadStateListener: add status to monitor:

adapter.addLoadStateListener {
            when (it.refresh) {
                is LoadState.Loading -> {}
                is LoadState.NotLoading -> {}
                is LoadState.Error -> {}
            }
        }
Copy the code

The state returns the parameter CombinedLoadStates, including refresh, prepend, Append,source, and Mediator

Each behavior is divided into three states:

  • LoadState.Loading (callback when Loading data)
  • NotLoading loadstate.notloading (callback before loading data and after loading data)
  • Loadstate. Error failed to load (callback failed to load data)

Our general business is focused on refreshing and reloading more;

Take SmartRefreshLayout as an example:

Pull-down refresh state processing:

// Because LoadState.NotLoading is also called before refreshing, So use an external variable after identifying the refresh var hasRefreshing = false adapter. AddLoadStateListener {the when (it. Refresh) {is LoadState. Loading - > { HasRefreshing = true // The loading page is not displayed if (srl_refresh. State! = RefreshState.Refreshing) { statePager.showLoading() } } is LoadState.NotLoading -> { if (hasRefreshing) { HasRefreshing = false statepager.showContent () srl_refresh. FinishRefresh (true) The first page will not trigger the append the if (it) source. Append. EndOfPaginationReached) {/ / no more append (can only use the source) srl_refresh.finishLoadMoreWithNoMoreData() } } } is LoadState.Error -> { statePager.showError() srl_refresh.finishRefresh(false) } } }Copy the code

Pull-up load more state handling:

// Because LoadState.NotLoading is also called before refreshing, So use an external variable to judge whether after Loading more var hasLoadingMore = false adapter. AddLoadStateListener {the when (it. Append) {is LoadState. Loading - > {hasLoadingMore = true // Reset the pull-up loading state, Loading srl_refresh. ResetNoMoreData ()} is LoadState.NotLoading -> {if (hasLoadingMore) {hasLoadingMore = false if (it. Source. Append. EndOfPaginationReached) {/ / no more (can only use the source of the append) srl_refresh. FinishLoadMoreWithNoMoreData ()} else { srl_refresh.finishLoadMore(true) } } } is LoadState.Error -> { srl_refresh.finishLoadMore(false) } } }Copy the code

LoadState.Loading state of append is not triggered if there is no more data on the first page.

Handling the refresh failure:

Just call refresh

adapter.refresh()
Copy the code

Load more failure handling:

srl_refresh.setOnLoadMoreListener { 
    adapter.retry()
}
Copy the code

Why retry? Paging is seamless loading, so no manual pull-up loading logic is retry(). However, Paging has been processed and will be retried only after a failure. Therefore, there is no problem with pull-up loading call retry

About headers and footers

PagingDataAdapter supports adding headers and footers

adapter.withLoadStateHeader(header: LoadStateAdapter<*>)
adapter.withLoadStateFooter(header: LoadStateAdapter<*>)
adapter.withLoadStateHeaderAndFooter(header: LoadStateAdapter<*>,
        footer: LoadStateAdapter<*>)
Copy the code

LoadStateAdapter: also a recyclerView. Adapter; Similar to multiple entries layout, just divided into a plurality of Adapter Google out of a MergeAdapter, is to combine a plurality of RecyclerView.Adapter into a, interested partners can search. I won’t introduce it here;

Example address:

github