Nutcracker

Nutcracker

Android リストのプリロード分析

今日のモバイルアプリ開発において、リストコントロールは最も一般的な UI コントロールの一つであり、画像、テキスト、動画などのさまざまな情報を表示できます。しかし、モバイルデバイス上でのリストデータの読み込みと表示は非常にリソースを消費する操作です。リスト内のデータ量が多い場合、ユーザーは完全なリストを見るために長い時間待たなければならないことがよくあります。ユーザー体験を向上させるために、開発者はプリロードのような読み込み時間を短縮するためのいくつかの戦略を採用する必要があります。
プリロードとは、ユーザーがリストをスクロールする前に、リストデータの一部を事前に読み込むことを指し、ユーザーがこれらのデータにスクロールしたときに即座に表示できるようにし、ユーザーの体験と認知速度を向上させます。Android システムはリストのプリロードを実現するためのいくつかの API と技術を提供しており、本課題は Android リストのプリロードについて深く研究し分析し、その実装原理、最適化戦略、性能への影響を探求し、開発者に参考とガイダンスを提供することを目的としています。

次に、いくつかのプリロードリストの方法を深く研究します。これらは Android システムの API 拡張やさまざまなサードパーティフレームワークの完全な実装を通じて行われ、実装原理を理解し、具体的なビジネスニーズと性能要件に基づいて総合的な評価と比較を行い、リストのプリロード機能を実現するための最も適切なソリューションを選択します。

RecyclerView.OnScrollListener#

RecyclerView の addOnScrollListener インターフェースを使用することで、RecyclerView のスクロール状態を監視し、RecyclerView に装載された LayoutManager を通じて現在スクロールされている最下部または最上部に表示されているアイテムのインデックスを取得し、そのインデックスを使用してプリロードロジックを実行する必要があるかどうかを判断できます。

rvList.addOnScrollListener(new RecyclerView.OnScrollListener() {
    @Override
    public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
        super.onScrolled(recyclerView, dx, dy);
        // LayoutMangerを取得
        RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
        // LayoutManagerが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();
            }
        }
    }
});

リストページの RecyclerView にこのコードを追加することで、リストのプリロードを簡単に実現できます。ローカルデータキャッシュがあり、ネットワーク状態を除外した場合、以前のフレームワークのloadMoreトリガーとの明らかな違いがあります。しかし、同時にログを確認すると、onScrolledメソッドが一度のスクロールで何度も呼び出され、同じページの読み込みが何度も呼び出されることが容易にわかります。ネットワーク読み込みは性能やデータトラフィックに大きな影響を与えるため、ビジネスロジックで重複処理を行う必要があります。

結論#

RecyclerView.onScrollListenerの実装と統合方法は大体上記の通りです。実践を通じて以前の結論を得ることができます:

利点

  • コーディングが簡単
  • コードの侵入性が低く、既存の RecyclerView やアダプタを変更する必要がない

欠点

  • onScrolledメソッドが一度のスクロールで何度も呼び出されるため、ビジネスロジックで重複判断を行う必要がある
  • 異なるLayoutManagerには異なる判断ロジックがあり、常に互換性を持たせる必要がある

Adapter.onBindViewHolder#

リストを監視する理由は、現在どのアイテムにスクロールしているかを知り、プリロードを開始するかどうかを決定するためです。そのためには、スクロール状態とLayoutManagerを取得する必要があります。実際、Adapter には生まれつき簡単に使えるコールバックがあり、それがonBindViewHolderです。onBindViewHolderRecyclerViewが指定されたPositionのデータを表示する必要があるときに通知されます。この時、BindViewHolderPositionとリスト全体のデータを比較して、プリロードが必要かどうかを判断できます。リストのスクロール状態やLayoutManagerのタイプをリアルタイムで気にする必要はありません。

次に、Adapter の onBindViewHolder を使用して以下のプリロードを実現します:

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();
     }
    }
}

コード全体の実装もシンプルで理解しやすく、リストがスクロールする過程で全てのリスト項目の読み込みはAdapter.onBindViewHolderを通じて行われ、プリロード検出が一貫してトリガーされます。RecyclerView のロジック処理により、onBindViewHolderは一度のスクロールで何度も呼び出されることはありません。また、Adapter のポジション取得は LayoutManager に依存しないため、LayoutManager 関連のコードロジックも必要ありません。しかし、このソリューションにも利点と欠点があり、プリロードをトリガーする viewHolder がリストの読み込み中に RecyclerView のキャッシュ領域から上にスクロールされた場合、再度下にスクロールしてページの末尾に到達すると再びバインドされ、onBindViewHolderがトリガーされ、プリロードが重複してトリガーされることになります。

結論#

Adapter.onBindViewHolderの実装も複雑ではなく、リストの監視実装と比較して明らかな違いがあります。

利点

  • コードの記述ロジックがシンプルで、基底クラスを多くの場所で再利用でき、タイプに依存しない
  • RecyclerViewはページの先頭と末尾のViewHolderを即座に回収しないため、通常のスクロールイベント内で何度も読み込みをトリガーしない

欠点

  • 依然として上にスクロールして読み込みを再トリガーすることがあり、重複処理が必要です。

BaseRecyclerViewAdapterHelper#

BaseRecyclerViewAdapterHelperは強力で柔軟な RecyclerView アダプタであり、GitHub で 23.4K スターを持つライブラリで、多くの商業プロジェクトのアダプタがこれを採用しています。以下をBRVAHと呼びます。プリロードソリューションがどのように処理されているかを探求します。ドキュメントを確認すると、読み込みのロジックは特にQuickAdapterHelper.ktで実装されていることがわかります。プリロードソリューションを直接確認します:

前のページを読み込む#

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) }

次のページを読み込む#

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は全体的に読み込みのソリューションとしてConcatAdapterを採用しており、ヘッダーとフッターの読み込みは独立したアダプタを通じてロジック制御を行っています。そのため、両部のコードは基本的に一致します。ここでは次のページの読み込みロジックを整理します。contentAdapterは実際のリストアダプタであり、trailingLoadStateAdapterはリストの末尾のロジック処理を専門に担当するアダプタです。OnViewAttachStateChangeListenerを作成し、viewHolder の onViewAttachedToWindow を監視し、アダプタのonViewAttachedToWindowにバインドされ、これを通じてプリロード監視メカニズムをトリガーします。具体的な実装は深く掘り下げませんが、トリガーの契機と大まかな実装を理解できれば十分です。興味がある方は、対応するソースコードを直接確認してください。その実際の実装もそれほど複雑ではありません。

BRVAHはアダプタのプリロードロジックをラッピングしているものの、アダプタのonBindViewHolderをトリガーの契機として使用するのではなく、onViewAttachedToWindowを使用しています。この違いは何かを見てみましょう。

onViewAttachedToWindowメソッドは、RecyclerView に ViewHolder を表示する際に呼び出されます。RecyclerView が新しい ViewHolder を表示する必要があるとき、アダプタのonCreateViewHolderメソッドを呼び出して ViewHolder を作成し、その ViewHolder をデータソースの対応するデータにバインドし、最後に ViewHolder のonBindViewHolderメソッドを呼び出してデータを表示します。この時、ViewHolder が RecyclerView に正常に追加されると、onViewAttachedToWindowメソッドが呼び出されます。

したがって、onViewAttachedToWindowメソッドは ViewHolder が RecyclerView に表示されるときにトリガーされ、onBindViewHolderメソッドは RecyclerView が ViewHolder のデータを更新する必要があるときにトリガーされます。

私たちのビジネスシーンから見ると —— プリロードの目的は、スクロールを通じてユーザーが下にスクロールする意図を持っている可能性を判断し、リストデータを事前に補充してユーザーの待機を避けることです。ビジネスシーンは実際には現在のビューが本当に画面に表示されているかどうかにはあまり依存していないため、ここではライフサイクルのより前の **onBindViewHolderではなく、より後のonViewAttachedToWindow** を使用しています。ソースコードの観点からは良い説明が得られず、リポジトリを確認しても関連するコミットコメントはありません。後で作者に連絡を取って確認できるかどうかを見てみるしかありません。

結論#

BRVAHは新しいプリロードソリューションを提供しており、現在のところ本質的には onBindViewHolder と類似していますが、BRVAHはこれに加えて重複読み込みを防ぐための一連のソリューションやリストのヘッダーとフッターの優雅な処理を提供しています。

利点

  • 統合の難易度は中程度
  • 高いスターと活発さを持ち、問題が発生する確率は低い
  • 一連のソリューションを提供し、輪を再発明することを避ける

欠点

  • 新しいライブラリを導入する必要があり、既存のアダプタを変更調整する必要がある

BRV#

BRVSmartRefreshLayoutフレームワークに基づく拡張ライブラリで、SmartRefreshLayoutの基盤の上にプリロード、デフォルトページ、固定タイトルなどの機能を提供し、BRVAHよりも強力な機能と実用性を持つとされています。そのプリロードロジックは以下の通りです:

/** onBindViewHolderイベントを監視 */
    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)
                    }
                }
            }
        }
    }

onBindViewHolder のトリガーのロジックは、基本的に私たちが書いた onBindViewHolder の監視ロジックと同じです。現在のトリガーされた bindViewHolder のポジションを通じてプリロードをトリガーするかどうかを判断し、読み込みのヘッダーとフッターはSmartRefreshLayoutに基づいており、ViewGroup に個別に追加および削除されます。

結論#

BRVのプリロードソリューションは、基本的に私たちが Adapter に基づいて構築したものと一致し、重複処理を追加しています。

利点

  • 一連のソリューションを提供し、データの重複を避ける

欠点

  • 統合の難易度が高く、SmartRefreshLayoutに依存しており、SmartRefreshLayoutライブラリを導入しない場合は別途導入が必要
  • 後方プリロードのみを提供し、前方プリロードはサポートしていない

Paging 3#

Paging ライブラリの概要 | Android 開発者 | Android Developers

Paging は Jetpack のコンポーネントで、ローカルおよびネットワークからのデータページを読み込み表示するために特化されており、データのプリロード機能も提供しています。公式のリスト読み込みソリューションとして、どのように実装されているのでしょうか。

Paging3は一整のリストソリューションとして、ページデータのメモリキャッシュ、内蔵のリクエスト重複情報削除機能、リフレッシュおよび再試行機能のサポートなどを提供しています。さらに、paging3はデータ処理実装に Flow を大量に使用しており、機能呼び出しスタックも非常に深いため、コードの読み取りが複雑です。ここでは、Paging3のプリロードの契機と判断ロジックを理解し、他のフレームワークと比較するために見ていきます。

トリガー契機#

まずはトリガー契機です。Paging3PagingDataAdapterを RecyclerView のアダプタとして提供しており、開発者はそれに基づくアダプタを使用してリストを適合させる必要があります。PagingDataAdapterは内蔵の diff メカニズムとリストデータの直接管理を行い、リストの設定と更新はsubmitDataメソッドを通じて行い、データの取得はgetItemメソッドを通じて行います。paging3のプリロードメカニズムはgetItemの具体的な実装に隠されており、リストデータは完全にカプセル化されているため、呼び出し側はgetItemを通じてリストデータを取得することしかできません。getItemを呼び出すのは通常onBindViewHolderの際であるため、paging3 のページングトリガー契機も基本的にonBindViewHolder方式に等しいです。

判断ロジック#

getItemメソッドがトリガーされると、paging3 はViewportHintのスナップショットを生成し、現在のリストの状態を記述するために使用します。また、これらの情報に基づいてプリロードをトリガーするかどうかを判断します。

/**
     * 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
            }
        }
    }

prependHintappendHintは本質的にそれぞれFlowであり、現在プリロードメカニズムに適合すると、これらはviewportHintを専用のPageFetcherSnapshotに送信し、将来的にリフレッシュイベントに変換され、全体のデータ読み込みプロセスに組み込まれます。

Paging3のプリロードメカニズムは大体このようなものです。詳細なメカニズムはコード量が多いため深入りしませんが、Paging3に不慣れで興味がある方は、公式の CodeLab を参照して深く理解することをお勧めします。

結論#

paging3のプリロードはライブラリ全体の氷山の一角に過ぎませんが、公式もonBindViewHolderをプリロードの判断契機として使用していることがわかり、私たちにより軽量なソリューションを選択するための一定の裏付けを提供しています。

利点

  • 完全なメカニズムと公式の裏付けがあり、問題が発生する確率は低い
  • プリロードにはロック処理が追加されており、マルチスレッドの同時実行を考慮し、発生する可能性のある複数のリクエスト問題を完全に解決しています。

欠点

  • 統合の難易度が非常に高く、paging3は一整のリストソリューションであり、各レベルのロジック変更が必要です。
  • コードは Kotlin、Flow、コルーチンで書かれており、プリロードとデバッグの適応性が低く、Java での接続が不親切です。

CodeLab#

Google は、開発者が Paging 3 を迅速に統合し使用する方法を学ぶための 2 つのガイド文書を提供しています。

Android Paging 基礎知識 | Android Developers

Android Paging Advanced Codelab | Android Developers

まとめ#

リストのプリロードを実現する過程で、適切な技術ソリューションを選択することは非常に重要です。今日は以下のいくつかのリストプリロードソリューションを紹介しました:

  1. onScrollListener 技術を使用すると、スクロールイベントを監視し、指定された位置にスクロールする前にデータを事前に読み込むことで、リストのプリロード機能を実現できます。その実装は簡単ですが、欠点も非常に明白です。
  2. BindViewHolder 技術を使用すると、ViewHolder をバインドする際にデータのプリロードを行い、リストデータの読み込み速度とユーザー体験を向上させることができます。そのロジックはシンプルで明確であり、多くのソリューションの核心ロジックです。自分でラッピングを考える場合は、これを基にするのが最良の選択です。
  3. BRV フレームワークはオープンソースの Android リストフレームワークで、SmartRefreshLayoutに基づいており、リストプリロードを含む多くの一般的なリスト機能を提供します。BRV フレームワークはリストプリロードを簡単に実現でき、グループ化、ドラッグなどの他の多くの機能も提供します。残念ながら、リストの前方プリロードはサポートしていません。
  4. Paging フレームワークは、Android 公式が提供するページング読み込みを実現するためのフレームワークです。リストプリロードを簡単に実現でき、ページング読み込み、データキャッシュなどの機能も提供します。
  5. BaseRecyclerViewAdapterHelper は軽量な RecyclerView アダプタで、RecyclerView リストを迅速に構築でき、リストプリロードなどの機能をサポートしています。

どのソリューションを選択してリストプリロードを実現するかは、具体的なビジネスニーズと性能要件に基づいて総合的に評価し比較する必要があり、最も適切なソリューションを選択してリストプリロード機能を実現します。

参考資料:#

別の視点から、超シンプルな RecyclerView プリロード - 掘金

プリロード / プリフェッチ - BRV

Paging | Android 開発者 | Android Developers

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。