今日のモバイルアプリ開発において、リストコントロールは最も一般的な 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
です。onBindViewHolder
はRecyclerView
が指定されたPosition
のデータを表示する必要があるときに通知されます。この時、BindViewHolder
のPosition
とリスト全体のデータを比較して、プリロードが必要かどうかを判断できます。リストのスクロール状態や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#
BRV
はSmartRefreshLayout
フレームワークに基づく拡張ライブラリで、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
のプリロードの契機と判断ロジックを理解し、他のフレームワークと比較するために見ていきます。
トリガー契機#
まずはトリガー契機です。Paging3
はPagingDataAdapter
を 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
}
}
}
prependHint
とappendHint
は本質的にそれぞれ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
まとめ#
リストのプリロードを実現する過程で、適切な技術ソリューションを選択することは非常に重要です。今日は以下のいくつかのリストプリロードソリューションを紹介しました:
- onScrollListener 技術を使用すると、スクロールイベントを監視し、指定された位置にスクロールする前にデータを事前に読み込むことで、リストのプリロード機能を実現できます。その実装は簡単ですが、欠点も非常に明白です。
- BindViewHolder 技術を使用すると、ViewHolder をバインドする際にデータのプリロードを行い、リストデータの読み込み速度とユーザー体験を向上させることができます。そのロジックはシンプルで明確であり、多くのソリューションの核心ロジックです。自分でラッピングを考える場合は、これを基にするのが最良の選択です。
- BRV フレームワークはオープンソースの Android リストフレームワークで、
SmartRefreshLayout
に基づいており、リストプリロードを含む多くの一般的なリスト機能を提供します。BRV フレームワークはリストプリロードを簡単に実現でき、グループ化、ドラッグなどの他の多くの機能も提供します。残念ながら、リストの前方プリロードはサポートしていません。 - Paging フレームワークは、Android 公式が提供するページング読み込みを実現するためのフレームワークです。リストプリロードを簡単に実現でき、ページング読み込み、データキャッシュなどの機能も提供します。
- BaseRecyclerViewAdapterHelper は軽量な RecyclerView アダプタで、RecyclerView リストを迅速に構築でき、リストプリロードなどの機能をサポートしています。
どのソリューションを選択してリストプリロードを実現するかは、具体的なビジネスニーズと性能要件に基づいて総合的に評価し比較する必要があり、最も適切なソリューションを選択してリストプリロード機能を実現します。
参考資料:#
別の視点から、超シンプルな RecyclerView プリロード - 掘金