This article has been authorized “Yu Gang said” wechat public account exclusive release
2019/12/24 supplement
One year after the release of this article, I believe that this blog series should not be used as an introductory tutorial. Instead, readers who really want to understand the use of Paging should first try to understand the essence of its Paging component:
Reflection | Android list Paging component design and implementation of the Paging: system overview reflection | Android list Paging component design and implementation of the Paging: architecture design and principle of parsing
The above two articles provide a systematic overview of the Paging component, and I strongly recommend that readers consider the above two articles to be the highest priority for learning Paging. All other Paging Chinese blogs should have lower priority.
This article is related to the following:
Official Android architecture component Paging: Design aesthetics of Paging libraries Official Android architecture component Paging-Ex: Adds headers and footers to Paging lists Official Android architecture component Paging-Ex: responsive management of list states
An overview of the
Paging is a Paging library for Android native development that Google introduced at I/O 2018. If you are not familiar with Paging architecture components, please check out this article.
Paging: The design aesthetics of the Paging library, an official Android architecture component
The author has used Paging in the actual project for more than half a year. Compared with other popular Paging libraries in the market, the biggest highlight of Paging is that it encapsulates the list Paging loading logic into the DataSource as a callback function. After the configuration is complete, the developer does not need to control the Paging loading. The list automatically loads the next page of data and displays it.
This article explains the entire process of adding headers and footers to lists that use Paging, some of the obstacles encountered, and how you worked around them – if you want to browse through the final solution, go to the final solution section of this article.
The initial idea
Adding a RecyclerView Header or Footer to the RecyclerView list is not a very cumbersome thing. The simplest way is to put the RecyclerView and Header together in a subview of the same ScrollView. But it turns a blind eye to RecyclerView’s own reuse mechanism, so this solution is not preferred.
A more suitable solution is to implement a MultiType list. In addition to the Item type of the list itself, the Header or Footer is also considered an Item. There are many articles on the implementation of this method.
Before we start this article, let’s take a look at the final implementation. We add a Header and Footer to a Student’s paging list:
To achieve this effect, the author’s original idea was to implement Header and Footer through multi-type lists, but we soon ran into the first problem, which was that we did not directly hold the data source.
1. The data source is faulty
For regular multi-type lists, we can easily hold List
, from the data control, I prefer to use a placeholder representing the Header or Footer inserted at the top or bottom of the data List, so for RecyclerView rendering, it looks like this:
As I noted, an ItemData in the List
corresponds to an ItemView — I think it’s entirely worthwhile to create a separate Model type for each Header or Footer. It greatly improves the readability of the code, and for complex headers, The Model class that represents the state is also easier for developers to render.
This implementation is simple, readable and elegant, but in Paging, the idea is blocked in the first place.
Let’s first look at the PagedListAdapter class declaration:
// The T generic type represents the type of the data source, which is Student in this article
public abstract class PagedListAdapter<T.VH extends RecyclerView.ViewHolder>
extends RecyclerView.Adapter<VH> {
// ...
}
Copy the code
Therefore, we need to implement this:
// We can only specify the Student type
class SimpleAdapter : PagedListAdapter<Student, RecyclerView.ViewHolder>(diffCallback) {
// ...
}
Copy the code
We can specify the Student as an interface (such as an ItemData interface), and then have the Student and the Model corresponding to the Header implement this interface, and then do this:
class SimpleAdapter : PagedListAdapter<ItemData, RecyclerView.ViewHolder>(diffCallback) {
// ...
}
Copy the code
It does seem to work, but there’s one problem that we’ve overlooked, and that’s what this section is about:
We don’t hold the data directly.
Back to the original idea, we know that the most important feature of Paging is automatic Paging loading. This is the embodiment of the observer mode. After configuration, we don’t care how data is Paging, when it is loaded, or how it is rendered. So we don’t need to hold the List
directly (we can’t), let alone add headerItems and FooterItems to it manually.
In the case of this article, virtually all the logic is handed over to the ViewModel:
class CommonViewModel(app: Application) : AndroidViewModel(app) {
private val dao = StudentDb.get(app).studentDao()
fun getRefreshLiveData(a): LiveData<PagedList<Student>> =
LivePagedListBuilder(dao.getAllStudent(), PagedList.Config.Builder()
.setPageSize(15) // Configure the number of pages to load
.setInitialLoadSizeHint(40) // Initialize the number of loads
.build()).build()
}
Copy the code
As you can see, we don’t hold the List
directly, so the list.add(headerItem) scenario of holding and modifying the data source is hardly feasible (in fact, it is, but it is too expensive to go into this article).
2. Try implementing lists directly
Next, I’ll try to implement the multitype list directly. Instead of discussing how to implement the Footer, let’s implement the Header as follows:
class HeaderSimpleAdapter : PagedListAdapter<Student, RecyclerView.ViewHolder>(diffCallback) {
// 1. Assign a type to an item based on position
// If position = 1, treat it as a Header
// If position! = 1, is considered as a normal Student
override fun getItemViewType(position: Int): Int {
return when (position == 0) {
true -> ITEM_TYPE_HEADER
false -> super.getItemViewType(position)
}
}
// 2. Generate the corresponding ViewHolder according to different viewtypes
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
ITEM_TYPE_HEADER -> HeaderViewHolder(parent)
else -> StudentViewHolder(parent)
}
}
// 3. Render according to the holder type
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is HeaderViewHolder -> holder.renderHeader()
is StudentViewHolder -> holder.renderStudent(getStudentItem(position))
}
}
// 4.
// Get the student at position-1
private fun getStudentItem(position: Int): Student? {
return getItem(position - 1)}// 5. The number of items is one more because of the Header
override fun getItemCount(a): Int {
return super.getItemCount() + 1
}
// Omit other code...
// Omit the ViewHolder
}
Copy the code
The code and comments have made my personal idea clear. We’ve fixed a Header at the top of the list of types, which causes us to override the getItemCount() method, and in the onBindViewHolder() method that renders the Item, To add extra processing to the Sutdent retrieval — because of the extra Header, there is a mismatch between the data source and the list — when the NTH data is retrieved, we should render it at the NTH +1 position in the list.
I have drawn a simple diagram to describe this process, which may be more intuitive:
After writing the code, my intuition tells me that there seems to be no problem, so let’s see how it actually works:
Gif may not be so clear, but to summarize, there are two problems:
- 1. When we do the pull-down refresh, because
Header
More like oneStatically independent componentsBut it’s actually also part of the list, so the white light flashes, exceptStudent
List,Header
As aItem
Also refreshed, which was not what we expected; - 2. Instead of showing the list at the top, the list slides to a strange position after the drop-down refresh.
The root cause of both problems is still the problem with Paging calculating the position of a list:
For problem 1, the refresh of Paging for list is understood as the refresh of all items, so the Header, also used as Item, cannot avoid being refreshed.
Problem 2 is also caused by the same problem. When Paging obtains the first page of data (assuming there are only 10 pieces of data on the first page), Paging will command to update position in 0.. 9 Item, and actually because of the Header, we expect it to update position in 1.. 10 items, eventually causing problems with the refresh and presentation of new data.
3. Ask Google and Github for answers
As the title suggests, I tried Google and Github and finally found this link:
PagingWithNetworkSample – PagedList RecyclerView scroll bug
If you have examined the source code of PagedListAdapter briefly, you should know that PagedListAdapter internally defines an AsyncPagedListDiffer, which is used to load and display list data, and that PagedListAdapter is more of a shell. All paging-related logic is actually delegated to AsyncPagedListDiffer:
public abstract class PagedListAdapter<T.VH extends RecyclerView.ViewHolder>
extends RecyclerView.Adapter<VH> {
final AsyncPagedListDiffer<T> mDiffer;
public void submitList(@Nullable PagedList<T> pagedList) {
mDiffer.submitList(pagedList);
}
protected T getItem(int position) {
return mDiffer.getItem(position);
}
public int getItemCount(a) {
return mDiffer.getItemCount();
}
public PagedList<T> getCurrentList(a) {
returnmDiffer.getCurrentList(); }}Copy the code
Although we have no control over the acquisition and display of data in Paging, we can try to fool the PagedListAdapter, even if Paging gets position in 0.. 9 List, but we let the PagedListAdapter to update position in 1.. Isn’t 10 item ok?
Therefore, in the above Issue link, Onlymash proposed a solution:
rewritePagedListAdapter
In theAsyncPagedListDiffer
Proxy for all methods and then instantiate a new oneAsyncPagedListDiffer
And let the new Differ proxy these methods.
Due to space constraints, we will only show some of the core code:
class PostAdapter: PagedListAdapter<Any, RecyclerView.ViewHolder>() {
private val adapterCallback = AdapterListUpdateCallback(this)
// Update the NTH +1 position when the NTH data is fetched
private val listUpdateCallback = object : ListUpdateCallback {
override fun onChanged(position: Int, count: Int, payload: Any?). {
adapterCallback.onChanged(position + 1, count, payload)
}
override fun onMoved(fromPosition: Int, toPosition: Int) {
adapterCallback.onMoved(fromPosition + 1, toPosition + 1)}override fun onInserted(position: Int, count: Int) {
adapterCallback.onInserted(position + 1, count)
}
override fun onRemoved(position: Int, count: Int) {
adapterCallback.onRemoved(position + 1, count)
}
}
// Create a differ function
private val differ = AsyncPagedListDiffer<Any>(listUpdateCallback,
AsyncDifferConfig.Builder<Any>(POST_COMPARATOR).build())
// Override all methods and delegate all methods to the new Differ
override fun getItem(position: Int): Any? {
return differ.getItem(position - 1)}// Override all methods and delegate all methods to the new Differ
override fun submitList(pagedList: PagedList<Any>? {
differ.submitList(pagedList)
}
// Override all methods and delegate all methods to the new Differ
override fun getCurrentList(a): PagedList<Any>? {
return differ.currentList
}
}
Copy the code
Now that we’ve successfully implemented the above idea, a picture is worth a thousand words:
4. Another implementation
The implementation solution in the previous section is completely feasible, but in my opinion, the fly in the ointment is that this solution changes the existing Adapter code too much.
I built a AdapterListUpdateCallback, a ListUpdateCallback, and a new AsyncPagedListDiffer, pay equal attention to write too much PagedListAdapter – I added dozens of pages related to generation Code, but these codes are not directly related to normal list presentation.
Sure, I could pull all this logic out and put it in a new class, but I still feel like I’m copying and rewriting a new PagedListAdapter class, so what else is there to think about?
Finally I found this article:
Android RecyclerView + Paging Library Add header refresh will automatically scroll problem analysis and solution
In this article, the author obtains a simpler scheme to implement Header through detailed analysis of the source code of Paging. Students who are interested in Paging can click on it to check. Here is a brief explanation of its principle:
By looking at the source code and adding Paging, for example, Paging pair Paging pair Paging pair Updates on the list is actually invoked the RecyclerView. The Adapter notifyItemRangeInserted () method, and we can rewrite the Adapter. RegisterAdapterDataObserver () method, Adjust the data update logic:
/ / 1. Create a new AdapterDataObserverProxy RecyclerView. The class hierarchy AdapterDataObserver
class AdapterDataObserverProxy extends RecyclerView.AdapterDataObserver {
RecyclerView.AdapterDataObserver adapterDataObserver;
int headerCount;
public ArticleDataObserver(RecyclerView.AdapterDataObserver adapterDataObserver, int headerCount) {
this.adapterDataObserver = adapterDataObserver;
this.headerCount = headerCount;
}
@Override
public void onChanged(a) {
adapterDataObserver.onChanged();
}
@Override
public void onItemRangeChanged(int positionStart, int itemCount) {
adapterDataObserver.onItemRangeChanged(positionStart + headerCount, itemCount);
}
@Override
public void onItemRangeChanged(int positionStart, int itemCount, @Nullable Object payload) {
adapterDataObserver.onItemRangeChanged(positionStart + headerCount, itemCount, payload);
}
// Update the NTH +1 position when the NTH data is fetched
@Override
public void onItemRangeInserted(int positionStart, int itemCount) {
adapterDataObserver.onItemRangeInserted(positionStart + headerCount, itemCount);
}
@Override
public void onItemRangeRemoved(int positionStart, int itemCount) {
adapterDataObserver.onItemRangeRemoved(positionStart + headerCount, itemCount);
}
@Override
public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
super.onItemRangeMoved(fromPosition + headerCount, toPosition + headerCount, itemCount); }}/ / 2. For the Adapter, only need to rewrite registerAdapterDataObserver () method
// Then use AdapterDataObserverProxy as the proxy
class PostAdapter extends PagedListAdapter {
@Override
public void registerAdapterDataObserver(@NonNull RecyclerView.AdapterDataObserver observer) {
super.registerAdapterDataObserver(newAdapterDataObserverProxy(observer, getHeaderCount())); }}Copy the code
We pulled out the extra logic as a new class, in much the same way as in the previous section, and we got the expected result.
After tracing the source code, there is no difference in performance between the two implementations. The only difference is that the former rewrite the PagedListAdapter and give AsyncPagedListDiffer to update the Item. In this way, AsyncPagedListDiffer’s internal logic for updating an Item is still given to the notifyItemRangeInserted() method of RecyclerView.adapter — there is essentially no difference between the two.
5. Final solution
Although the preceding paragraph only describes how a Paging library implements headers, the same is true for a Footer, which can also be considered a different type of Item. Also, because Footer is at the bottom of the list, it doesn’t affect position updates, so it’s simpler.
Here is a complete Adapter example:
class HeaderProxyAdapter : PagedListAdapter<Student, RecyclerView.ViewHolder>(diffCallback) {
override fun getItemViewType(position: Int): Int {
return when (position) {
0 -> ITEM_TYPE_HEADER
itemCount - 1 -> ITEM_TYPE_FOOTER
else -> super.getItemViewType(position)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
ITEM_TYPE_HEADER -> HeaderViewHolder(parent)
ITEM_TYPE_FOOTER -> FooterViewHolder(parent)
else -> StudentViewHolder(parent)
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is HeaderViewHolder -> holder.bindsHeader()
is FooterViewHolder -> holder.bindsFooter()
is StudentViewHolder -> holder.bindTo(getStudentItem(position))
}
}
private fun getStudentItem(position: Int): Student? {
return getItem(position - 1)}override fun getItemCount(a): Int {
return super.getItemCount() + 2
}
override fun registerAdapterDataObserver(observer: RecyclerView.AdapterDataObserver) {
super.registerAdapterDataObserver(AdapterDataObserverProxy(observer, 1))}companion object {
private val diffCallback = object : DiffUtil.ItemCallback<Student>() {
override fun areItemsTheSame(oldItem: Student, newItem: Student): Boolean =
oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: Student, newItem: Student): Boolean =
oldItem == newItem
}
private const val ITEM_TYPE_HEADER = 99
private const val ITEM_TYPE_FOOTER = 100}}Copy the code
If you’d like to see the full demo run, here’s the address of the sample for this article:
Github.com/qingmei2/Sa…
6. More optimization?
Is there more room for optimization in the final scheme at the end of the paper? Of course, in a real project, it makes more sense to simply encapsulate it (such as the Builder mode, encapsulating a Header, Footer or even a decorator class with both, or whatever…). .
The purpose of this article is to describe the problems encountered in the use of Paging and the process of solving them, so project-level encapsulation and implementation details are not the main content of this article. As for the implementation of Header and Footer in Paging, if you have a better solution, look forward to discussing with you.
Reference & Thanks
- The Paging library source code
- Android RecyclerView + Paging Library Add header refresh will automatically scroll problem analysis and solution
- PagingWithNetworkSample – PagedList RecyclerView scroll bug
series
Create the best series of Android Jetpack blogs:
- A detailed analysis of the official Android architecture component Lifecycle
- Android’s official architecture component ViewModel: From the past to the present
- LiveData, the official Android architecture component: Two or three things in the Observer pattern area
- Paging: The design aesthetics of the Paging library, an official Android architecture component
- Official Android architecture component Paging-Ex: Adds a Header and Footer to the Paging list
- Official Architecture component of Android Paging-Ex: reactive management of list states
- Navigation: a handy Fragment management framework
- DataBinding-Ex is an official Architecture component for Android
Jetpack for Android
- Open source project: Github client implemented by MVVM+Jetpack
- Open source project: Github client based on MVVM, MVI+Jetpack implementation
- Summary: Using MVVM to try to develop a Github client and some thoughts on programming
About me
If you think this article is of value to you, please feel free to follow me at ❤️ or on my personal blog or Github.
If you think the writing is a little bit worse, please pay attention and push me to write a better writing — just in case I do someday.
- My Android learning system
- About article correction
- About Paying for Knowledge