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, becauseHeaderMore like oneStatically independent componentsBut it’s actually also part of the list, so the white light flashes, exceptStudentList,HeaderAs aItemAlso 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:

rewritePagedListAdapterIn theAsyncPagedListDifferProxy for all methods and then instantiate a new oneAsyncPagedListDifferAnd 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