3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Paging 3 + LazyColumn によるオフセットベースのページネーションの注意点

Last updated at Posted at 2025-08-15

Jetpack Compose でページングによるアイテムの取得・表示を行う場合、Paging3 を利用すれば、容易に実現することができます。ただそのページングの性質によってはクラッシュの発生原因になる危険性も秘めています。本記事では注意したい使い方について紹介をします。

Paging3とLazyColumnを使っている実装でIllegalArgumentExceptionが発生

Paging3で取得したデータをLazyColumnで表示する画面にてjava.lang.IllegalArgumentException: Key was already used on another itemというエラーによるクラッシュが報告されました。

原因は、、、

エラーメッセージからLazyColumnに設定しているキーが重複していることが原因だとすぐにわかりました。
しかし、LazyColumnのキーにはアイテムのユニークなIDを指定しており、データソース上にも重複したデータは存在していませんでした。

ではなぜキーの重複が発生したのでしょうか。原因をさらに探ると、APIのページングの仕組みと、データの更新頻度の高さが関係していることがわかりました。

LazyColumnのキーの指定について

LazyColumnLazyRowといった遅延読み込みを行うコンポーネントでは、itemsの引数にkeyを指定することができます。

LazyColumn {
    items(
        items = items,
        key = { item -> item.id } // itemのidをキーとして指定
    ) { item ->
        ItemRow(item)
    }
}

キーを指定すると、Composeは各アイテムを識別し、スクロール時のパフォーマンスを最適化したり、アイテムの追加や削除、並び替えがあった場合に賢く再コンポーズを行ったりすることができます。しかし、このキーに重複した値が渡されると、どのアイテムに対応するコンポーザブルなのかを正確に判断できなくなり、IllegalArgumentExceptionが発生します。

API側のページングの指定方法

今回のAPIは、リクエストパラメータとして「ページの番号」と「1ページあたりのアイテム数」を指定する、一般的なオフセットベースのページング方式を採用していました。
例えば、「2ページ目の20件」を取得する場合、page=2&limit=20のようなリクエストを送ります。

この方式はシンプルですが、頻繁に新しいデータが追加されるリストの場合に問題が発生します。
ユーザーが1ページ目を読み込み、次に2ページ目をリクエストするまでの間に新しいアイテムがリストの先頭に追加されたとします。すると、元々1ページ目の末尾にあったアイテムが2ページ目の先頭にずれてしまい、結果としてクライアントは同じアイテムを2回受け取ることになります。

これが、LazyColumnでキーの重複が発生していた真の原因でした。

回避策

原因がわかったところで、いくつかの解決策を検討しました。

filterで重複したアイテムを取り除く❌

最初に思いついたのは、PagingDatamapfilterで加工して重複を排除することでした。
しかし、Paging3のPagingDataは、それ自体がデータのスナップショットであり、ページ単位で独立しています。そのため、ページをまたいで重複をチェックし、データストリーム全体から動的にアイテムをフィルタリングすることは困難です。PagingDataの変換処理はサポートされていますが、このようなケースでの重複排除には向いていません。

LazyColumnのキーの指定をなくす🤔

次に考えられるのは、LazyColumnkeyの指定を単純にやめることです。

LazyColumn {
    items(
        items = items
        // key = { item -> item.id } // キーの指定をやめる
    ) { item ->
        ItemRow(item)
    }
}

これによりクラッシュは回避できます。しかし、キーを指定することによるパフォーマンス上のメリットを失うことになり、特にリストのアイテムが複雑な場合や、スクロール位置を正確に復元したい場合には望ましくありません。あくまで一時的な回避策であり、根本的な解決とは言えません。

API側の修正😃

最も根本的な解決策は、APIのページング方式を修正することです。
オフセットベースではなく、カーソルベース(またはキーセット)のページングを導入します。これは、最後に取得したアイテムのID(やタイムスタンプなど)を次のリクエストのパラメータに含める方式です。

例えば、「item_id=123以降の20件」を取得する場合、since_id=123&limit=20のようなリクエストを送ります。
この方式であれば、リクエストの合間に新しいアイテムが追加されたとしても、取得するデータの範囲が明確であるため、アイテムの重複や欠落が発生しません。

まとめ

Paging3とLazyColumnの組み合わせは非常に強力ですが、扱うデータの性質によっては思わぬ落とし穴があります。
特に、SNSのタイムラインのようにリアルタイム性が高く、頻繁にデータが更新されるリストを扱う場合は注意が必要です。

  • LazyColumnのキー重複IllegalArgumentExceptionを引き起こす。
  • オフセットベースのページングは、データが頻繁に更新される場合にアイテムの重複を発生させる可能性がある。
  • 根本的な解決策は、APIをカーソルベースのページングに変更すること。

Paging3を導入する際は、利用するAPIのページング方式が、扱うデータの特性に適しているかを事前に確認することが重要です。

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?