Nutcracker

Nutcracker

Analysis of Android List Preloading

In today's mobile application development, list controls are one of the most commonly used UI controls, capable of displaying various information such as images, text, videos, and more. However, loading and displaying list data on mobile devices is a resource-intensive operation. When the amount of data in a list is large, users often have to wait a long time to see the complete list. To improve user experience, developers need to adopt strategies to reduce loading time, such as preloading.
Preloading refers to loading a portion of the list data in advance before the user scrolls through the list, so that when the user scrolls to this data, it can be displayed immediately, thus enhancing the user's experience and perceived speed. The Android system provides several APIs and technologies to implement list preloading. This topic aims to conduct an in-depth study and analysis of Android list preloading, exploring its implementation principles, optimization strategies, and performance impacts, providing references and guidance for developers.

Next, we will delve into several methods of preloading lists, either through extensions of the Android system's APIs or through various well-implemented third-party frameworks. We will understand their implementation principles to facilitate comprehensive evaluation and comparison based on specific business needs and performance requirements, thus selecting the most suitable solution to implement list preloading functionality.

RecyclerView.OnScrollListener#

By using the addOnScrollListener interface of RecyclerView, we can listen to the scrolling state of the RecyclerView and then obtain the index of the item currently displayed at the bottom or top of the scroll through the LayoutManager loaded by the RecyclerView, using this index to determine whether we need to execute the preloading logic.

rvList.addOnScrollListener(new RecyclerView.OnScrollListener() {
    @Override
    public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
        super.onScrolled(recyclerView, dx, dy);
        // Get LayoutManager
        RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
        // If LayoutManager is LinearLayoutManager
        if (layoutManager instanceof GridLayoutManager) {
            GridLayoutManager manager = (GridLayoutManager) layoutManager;
            int nextPreloadCount = 8;
            int previousPreloadCount = 4;
            if (dy > 0
                    && manager.findLastVisibleItemPosition()
                    == layoutManager.getItemCount() - 1 - nextPreloadCount) {
                mViewModel.loadNext();
            } else if (dy > 0 && manager.findFirstVisibleItemPosition() == previousPreloadCount) {
                mViewModel.loadPrevious();
            }
        }
    }
});

By adding this piece of code to the RecyclerView on the list page, we can easily implement list preloading. In the case of local data caching and excluding network status, there is a significant difference compared to the previous triggering of loadMore through the framework. However, at the same time, it is easy to find in the logs that the onScrolled method will be called multiple times during a single scroll, causing multiple calls for loading the same page, which has a significant impact on performance and data traffic under network loading, requiring deduplication in business logic.

Conclusion#

The implementation and integration method of RecyclerView.onScrollListener are roughly as described above. Through practice, we can draw the following conclusions:

Advantages:

  • Simple coding
  • Low code invasiveness, no need to modify existing RecyclerView or adapter

Disadvantages:

  • The onScrolled method will be called multiple times during a single scroll, requiring deduplication in business logic
  • Different LayoutManager will have different judgment logic, requiring continuous compatibility and extension

Adapter.onBindViewHolder#

We listen to the list to know which item is currently scrolled to, thus deciding whether to start preloading. For this, we need to obtain the scrolling state and LayoutManager. In fact, the Adapter has a naturally simple and easy-to-use callback, which is onBindViewHolder. onBindViewHolder is notified only when RecyclerView needs to display data for a specified Position. At this point, we can determine whether we need to perform preloading based on the Position of BindViewHolder and the comparison with the entire list's data, without needing to constantly monitor the scrolling state of the list and the type of LayoutManager.

Next, we will use the Adapter's onBindViewHolder to implement the following preloading:

public class ListAdapter extends RecyclerView.Adapter<ViewHolder> {

    private int nextPreloadCount = 8;

    private int previousPreloadCount = 4;
    
    private boolean isScroll;

    public void onBindViewHolder(@NonNull VH holder, int position,
                @NonNull List<Object> payloads) {
            checkPreload(position);
    }

    public void bindRecyelerView(@NonNull RecyclerView recyclerView) {
         if (newState == RecyclerView.SCROLL_STATE_IDLE) {
             isScroll = false;
         } else {
             isScroll = true;
         }
    }
    
    private void checkPreload(int position) {
         if (!isScroll) {
             return;
         }
         if (position == previousPreloadCount) {
              mViewModel.loadPrevious();
         } else if (position == getItemCount() - 1 - nextPreloadCount) {
              mViewModel.loadNext();
         }
    }
}

The overall implementation of the code is also simple and easy to understand. During the scrolling process of the list, all the loading of list items will go through Adapter.onBindViewHolder, thus triggering the preloading detection consistently. Due to the logical processing of RecyclerView, onBindViewHolder will not be called multiple times during a single scroll. Additionally, since the position obtained by the Adapter is independent of the LayoutManager, there is no need for code logic related to the LayoutManager. However, this solution still has its pros and cons; when the viewHolder that triggers preloading is scrolled out of the RecyclerView's cache area during the loading process, scrolling down to the end of the page will cause it to be bound again, leading to repeated triggering of onBindViewHolder.

Conclusion#

The implementation of Adapter.onBindViewHolder is not complicated, and compared to the implementation of listening to the list, the differences are significant.

Advantages:

  • Simple coding logic, base class can be reused in multiple places, independent of type
  • RecyclerView does not immediately recycle the ViewHolder at the beginning and end of the page, preventing multiple loads from being triggered during normal scrolling events

Disadvantages:

  • Still triggers pull-to-load again, requiring deduplication.

BaseRecyclerViewAdapterHelper#

BaseRecyclerViewAdapterHelper is a powerful and flexible RecyclerView Adapter, a library with 23.4K stars on GitHub, and many commercial project adapters use it, hereinafter referred to as BRVAH. Let's explore how it handles the preloading scheme. By checking the documentation, we find that its logic for loading more is specifically implemented in QuickAdapterHelper.kt. Let's directly look at its preloading scheme:

Load Previous Page#

leadingLoadStateAdapter?.let {
            mAdapter.addAdapter(it);

            firstAdapterOnViewAttachChangeListener =
                object : BaseQuickAdapter.OnViewAttachStateChangeListener {

                    override fun onViewAttachedToWindow(holder: RecyclerView.ViewHolder) {
                        leadingLoadStateAdapter.checkPreload(holder.bindingAdapterPosition);
                    }

                    override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) {

                    }
                }.apply { contentAdapter.addOnViewAttachStateChangeListener(this) }

Load Next Page#

trailingLoadStateAdapter?.let {
            mAdapter.addAdapter(it);

            lastAdapterOnViewAttachChangeListener =
                object : BaseQuickAdapter.OnViewAttachStateChangeListener {

                    override fun onViewAttachedToWindow(holder: RecyclerView.ViewHolder) {
                        trailingLoadStateAdapter.checkPreload(
                            holder.bindingAdapter?.itemCount ?: 0,
                            holder.bindingAdapterPosition
                        );
                    }

                    override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) {

                    }
                }.apply { contentAdapter.addOnViewAttachStateChangeListener(this) }
        }

BRVAH adopts the ConcatAdapter for loading schemes, and the loading of the header and footer is controlled by independent adapters, so the code for both parts is basically the same. Here we will sort out the logic for loading the next page. contentAdapter is the actual list adapter, while trailingLoadStateAdapter is specifically responsible for handling the logic at the end of the list. It creates an OnViewAttachStateChangeListener to listen for the onViewAttachedToWindow of the viewHolder. The specific implementation is bound to the Adapter's onViewAttachedToWindow, triggering the preloading monitoring mechanism. We won't delve into the specific implementation here; we just need to understand its triggering mechanism and general implementation. Those interested can directly check the corresponding source code, as its actual implementation is not complicated.

It can be seen that while BRVAH adopts encapsulation of the Adapter to handle preloading logic, it does not use the Adapter's onBindViewHolder as the triggering mechanism but instead uses onViewAttachedToWindow. What causes this difference? We can look at the actual differences between the two:

The onViewAttachedToWindow method is called when a ViewHolder is displayed in the RecyclerView. When the RecyclerView needs to display a new ViewHolder, it calls the Adapter's onCreateViewHolder method to create a ViewHolder, then binds this ViewHolder to the corresponding data in the data source, and finally calls the ViewHolder's onBindViewHolder method to display the data in the ViewHolder's view. At this point, if the ViewHolder is successfully added to the RecyclerView, the onViewAttachedToWindow method will be called.

Therefore, the onViewAttachedToWindow method is triggered when the ViewHolder is displayed on the RecyclerView, while the onBindViewHolder method is triggered when the RecyclerView needs to update the ViewHolder's data.

From our business scenario— the purpose of preloading is to determine the user's intent to scroll down by sliding, to preload list data in advance, and avoid user waiting. The business scenario does not heavily rely on whether the current view is actually displayed on the screen, so it does not use the earlier lifecycle onBindViewHolder but uses the later onViewAttachedToWindow. There is no good explanation from the source code perspective, and checking the repository does not provide relevant commit notes. We can only see if we can contact the author later for inquiries.

Conclusion#

BRVAH brings a new preloading scheme. Although it currently seems fundamentally similar to onBindViewHolder, BRVAH also provides a complete set of solutions, including preventing duplicate loading and elegantly handling the list header and footer.

Advantages:

  • Moderate integration difficulty
  • High stars and activity, low probability of issues
  • Provides a complete set of solutions, avoiding reinventing the wheel

Disadvantages:

  • Requires introducing a new library, modifying existing adapters

BRV#

BRV is an extension library based on the SmartRefreshLayout framework. It provides preloading, default pages, sticky headers, and other functions on top of SmartRefreshLayout, claiming to have more powerful features and practicality than BRVAH. Its preloading logic is as follows:

/** Listen for onBindViewHolder events */
var onBindViewHolderListener = object : OnBindViewHolderListener {
    override fun onBindViewHolder(
        rv: RecyclerView,
        adapter: BindingAdapter,
        holder: BindingAdapter.BindingViewHolder,
        position: Int,
    ) {
        if (mEnableLoadMore && !mFooterNoMoreData &&
            rv.scrollState != SCROLL_STATE_IDLE &&
            preloadIndex != -1 &&
            (adapter.itemCount - preloadIndex <= position)
        ) {
            post {
                if (state == RefreshState.None) {
                    notifyStateChanged(RefreshState.Loading)
                    onLoadMore(this@PageRefreshLayout)
                }
            }
        }
    }
}

It can be seen that the logic for listening to onBindViewHolder is basically similar to what we wrote. It determines whether to trigger preloading based on the current triggered bindViewHolder position, while the loading of the header and footer is based on SmartRefreshLayout, adding and removing through ViewGroup.

Conclusion#

The preloading scheme of BRV is basically consistent with our own based on the Adapter, adding deduplication processing on this basis.

Advantages:

  • Provides a complete set of solutions, avoiding duplicate data handling

Disadvantages:

  • Complex integration difficulty, depends on SmartRefreshLayout. If the SmartRefreshLayout library is not introduced, it needs to be added separately
  • Only supports backward preloading, does not support forward preloading

Paging 3#

Paging Library Overview | Android Developers

Paging, as a Jetpack component, is specifically designed for loading and displaying data pages from local and network sources. It also provides data preloading functionality. As the official list loading solution, how is it implemented?

Paging3 serves as a complete list solution, offering memory caching for paginated data, built-in request deduplication, and support for refresh and retry functions, among others. Additionally, paging3 extensively uses Flow for data processing, resulting in a complex function call stack that makes code reading quite challenging. Here, we will only understand the triggering mechanism and judgment logic of Paging3 preloading for comparison with other frameworks.

Triggering Mechanism#

First, regarding the triggering mechanism, Paging3 provides PagingDataAdapter as the adapter for RecyclerView. Developers must use an adapter based on it for list adaptation. PagingDataAdapter has a built-in diff mechanism and directly manages list data. List settings and updates need to be done through the submitData method, while data retrieval is done through the getItem method. The preloading mechanism of paging3 is hidden in the specific implementation of getItem. Since the list data is completely encapsulated, callers can only retrieve list data through getItem, and calling getItem often occurs during onBindViewHolder, so the triggering mechanism for paging3's pagination is essentially equivalent to the onBindViewHolder method.

Judgment Logic#

When the getItem method is triggered, paging3 generates a snapshot of ViewportHint to store the current state of the list and uses this information to determine whether to trigger preloading.

/**
     * Processes the hint coming from UI.
     */
    fun processHint(viewportHint: ViewportHint) {
        state.modify(viewportHint as? ViewportHint.Access) { prependHint, appendHint ->
            if (viewportHint.shouldPrioritizeOver(
                    previous = prependHint.value,
                    loadType = PREPEND
                )
            ) {
                prependHint.value = viewportHint
            }
            if (viewportHint.shouldPrioritizeOver(
                    previous = appendHint.value,
                    loadType = APPEND
                ) {
                appendHint.value = viewportHint
            }
        }
    }

prependHint and appendHint are essentially Flows. When they meet the preloading mechanism, they will send viewportHint to a specialized PageFetcherSnapshot for processing, which will eventually convert into a refresh event and integrate into the entire data loading process.

The preloading mechanism of Paging3 is roughly as described. Due to the large amount of code, we won't delve deeper. If you are not familiar with Paging3 and are interested, you can refer to the official CodeLab for further understanding.

Conclusion#

The preloading in paging3 is just the tip of the iceberg of the entire library. However, it can be seen that the official also uses onBindViewHolder as the judgment mechanism for preloading, providing some endorsement for selecting a lighter solution.

Advantages:

  • Complete mechanism and official endorsement, low probability of issues
  • Preloading also includes lock handling, considering multi-thread concurrency, completely resolving potential multiple request issues.

Disadvantages:

  • Very high integration difficulty, paging3 is a complete list solution requiring logical changes at various levels
  • Code is written in Kotlin, Flow, and coroutines, making pre-reading and debugging challenging, not friendly for Java integration

CodeLab#

Google provides two guiding documents to help developers quickly learn how to integrate and use Paging 3:

Android Paging Basics | Android Developers

Android Paging Advanced Codelab | Android Developers

Summary#

In the process of implementing list preloading, choosing the right technical solution is crucial. Today we introduced the following list preloading solutions:

  1. Using the onScrollListener technique, which allows for preloading data before scrolling to a specified position, thus achieving list preloading functionality. Its implementation is simple, but its drawbacks are quite obvious.
  2. Using the BindViewHolder technique, which allows for data preloading when binding the ViewHolder, thus improving the loading speed of list data and user experience. Its logic is straightforward and is the core logic of many solutions. If considering self-encapsulation, it is an excellent choice as a blueprint.
  3. The BRV framework is an open-source Android list framework based on SmartRefreshLayout, providing many common list features, including list preloading. The BRV framework can conveniently implement list preloading and offers many other features, such as grouping, dragging, etc. Unfortunately, it does not support forward preloading.
  4. The Paging framework is an official Android framework for implementing paginated loading. It conveniently implements list preloading while also providing pagination loading, data caching, and other features.
  5. BaseRecyclerViewAdapterHelper is a lightweight RecyclerView adapter that can quickly build RecyclerView lists and supports list preloading and other features.

Choosing which solution to implement list preloading requires comprehensive evaluation and comparison based on specific business needs and performance requirements to select the most suitable solution for implementing list preloading functionality.

References:#

Change a mindset, super simple RecyclerView preloading - Juejin

Preloading/Pre-fetching - BRV

Paging | Android Developers

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.