The contents of the list are paging data returned by the server, which is pulled down each time you browse to the end of the current page. This interrupts the user’s browsing and inevitably creates a wait. The product wants to make this process unconscious. One way to do this is to preload, which is to request the next page of data before the page is finished, giving the user the impression that the list is infinite.

Listen for list scrolling

The first thought of solution is to monitor list rolling state, execute when the list quickly scroll to the bottom, preload, RecyclerView. OnScrollListener provides two callback:

public class RecyclerView {
    public abstract static class OnScrollListener {
        public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState){}
        public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy){}}}Copy the code

LayoutManager can be reached in onScrolled(), which provides a number of methods related to the entry position:

// Add a new extension method for RecyclerView to listen for preloading events
fun RecyclerView.addOnPreloadListener(preloadCount: Int, onPreload: () -> Unit) {
    // Monitor RecyclerView state
    addOnScrollListener(object : RecyclerView.OnScrollListener() {
        override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
            super.onScrolled(recyclerView, dx, dy)
            / / get LayoutManger
            val layoutManager = recyclerView.layoutManager
            // If the LayoutManager is a LinearLayoutManager
            if (layoutManager is LinearLayoutManager) {
            	// If the list is scrolling up and the index value of the entry is equal to the preload threshold
                if (dy > 0 && layoutManager.findLastVisibleItemPosition() == layoutManager.itemCount - 1 - preloadCount) {
                    onPreload()
                }
            }
        }
    })
}
Copy the code

When the list is rolled, the system checks whether the index of the last visible entry in the list is equal to the preloading threshold. If the index is equal, the list is nearly rolled to the bottom, and the preloading callback is triggered. Preloading can then be implemented like this:

recyclerView.addOnPreloadListener(3) {// Perform preloading when there are three entries to the bottom of the list
    // Preload the business logic
}
Copy the code

A bug was detected when running the Demo: onPreload() was not executed when scrolling the list quickly, and onPrelaod() was executed multiple times when scrolling the list slowly.

The reason is that RecyclerView does not guarantee that onScrolled() will be called when every entry appears. If the scrolling is very fast, it is possible for an entry to miss the callback.

To avoid missing out, only relax the terms:

if (dy > 0 && layoutManager.findLastVisibleItemPosition() >= layoutManager.itemCount - 1 - preloadCount) {
    onPreload()
}
Copy the code

Change == to >=, on condition that it is relaxed, but the problem of multiple calls is worse. In the normal sliding process, this scheme cannot accurately match the preloading threshold, that is, onPreload() cannot be called back only once, because onScroll() is a callback of pixel granularity, while preloading requires entry granularity detection.

Another disadvantage of this scenario is coupling to the LayoutManager type. Code using the if (layoutManager is LinearLayoutManager) such judgment, if you want to fit StaggeredGridLayoutManager must add else branch, What if you add a custom LayoutManager?

Type independent preloading

Determine whether preload is the key to get table item index, just by layoutManager. FindLastVisibleItemPosition (), actually spared a big circle.

OnBindViewHolder (holder: ViewHolder, position: Int); onBindViewHolder(holder: ViewHolder, Int);

class PreloadAdapter: RecyclerView.Adapter<ViewHolder>() {
    // Preload the callback
    var onPreload: (() -> Unit)? = null
    // Preload the offset
    var preloadItemCount = 0
    // List scrolling status
    private var scrollState = SCROLL_STATE_IDLE
   
    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        checkPreload(position)
    }
    
    override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
        recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
            override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
                // Update the scroll status
                scrollState = newState
                super.onScrollStateChanged(recyclerView, newState)
            }
        })
    }
    
    // Determine whether to preload
    private fun checkPreload(position: Int) {
        if(onPreload ! =null
            && position == max(itemCount - 1 - preloadItemCount, 0)// The index value equals the threshold&& scrollState ! = SCROLL_STATE_IDLE// The list is scrolling) { onPreload? .invoke() } } }Copy the code

It can then be used like this:

val preloadAdapter = PreloadAdapter().apply {
    // Preload two entries from the end of the list
    preloadItemCount = 2
    onPreload = {
       // Preload the business logic}}Copy the code

This scheme has the following advantages:

  1. You don’t need to worry about how fast the list slides because all entries will go through onBindViewHolder(), and the index value and preload threshold can be determined using ==.

  2. Don’t worry about the user pulling up multiple times at the bottom of the list causing the callback to be preloaded multiple times, because onBindViewHolder() won’t be executed multiple times in this case.

  3. When RecyclerView replaces LayoutManager, there is no need to change the code.

The only thing to worry about is if you scroll to the bottom of the list once to trigger a preload and then scroll back (threshold entry rolls off the screen), assuming that the preload hasn’t finished yet and you scroll to the bottom again, onBindViewHolder() will need to be executed again to trigger another preload.

Of course, you can solve this problem by increasing the tag bit:

class VarietyAdapter: RecyclerView.Adapter<ViewHolder>() {
    // Add the preload status flag bit
    var isPreloading = false
   
    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        checkPreload(position)
    }
    
    // Determine whether to preload
    private fun checkPreload(position: Int) {
        if(onPreload ! =null
            && position == max(itemCount - 1 - preloadItemCount, 0)// The index value equals the threshold&& scrollState ! = SCROLL_STATE_IDLE// The list is scrolling
            && !isPreloading // Preloading is not in progress
        ) {
            isPreloading = true // Indicates that preloading is being performedonPreload? .invoke() } } }Copy the code

This flag bit is then controlled in the business layer, setting the flag position to false when the list content request succeeds, fails, or times out.

But I prefer to let the business layer maintain this bit, because if Adapter simply provides preload time, it doesn’t need to care when the business layer load ends.

Talk is cheap, show me the code

Recommended reading

RecyclerView series article directory is as follows:

  1. RecyclerView caching mechanism | how to reuse table?

  2. What RecyclerView caching mechanism | recycling?

  3. RecyclerView caching mechanism | recycling where?

  4. RecyclerView caching mechanism | scrap the view of life cycle

  5. Read the source code long knowledge better RecyclerView | click listener

  6. Proxy mode application | every time for the new type RecyclerView is crazy

  7. Better RecyclerView table sub control click listener

  8. More efficient refresh RecyclerView | DiffUtil secondary packaging

  9. Change an idea, super simple RecyclerView preloading

  10. RecyclerView animation principle | change the posture to see the source code (pre – layout)

  11. RecyclerView animation principle | pre – layout, post – the relationship between the layout and scrap the cache

  12. RecyclerView animation principle | how to store and use animation attribute values?

  13. RecyclerView list of interview questions | scroll, how the list items are filled or recycled?

  14. RecyclerView interview question | what item in the table below is recycled to the cache pool?

  15. RecyclerView performance optimization | to halve load time table item (a)

  16. RecyclerView performance optimization | to halve load time table item (2)

  17. RecyclerView performance optimization | to halve load time table item (3)

  18. How does RecyclerView roll? (a) | unlock reading source new posture

  19. RecyclerView how to achieve the scrolling? (2) | Fling

  20. RecyclerView Refresh list data notifyDataSetChanged() why is it expensive?