今回は、ページネーションについて解説します。
最初に
検索結果一覧など、ユーザーに対して複数のコンテンツを一度に表示する手段として、リストが挙げられます。 しかし、利用できるコンテンツが膨大なのに対して、端末で表示できるコンテンツ数はごく小さいことがあります。その場合、一度にすべてのコンテンツを読み込むと、UI/UX を損ない、また端末のリソースを無駄に消耗します。 今回、この問題の解決手段として、ページネーションを説明します。
対象範囲
今回は、ページネーションの概要と実例、そして実装例を説明します。また、ページネーションはモバイルアプリにて利用されることが多いため、Android アプリを例にして説明します。 なお、サーバーサイドにおけるページネーションの実装については省略します。
ページネーションとは?
ページネーションとは、複数のコンテンツを適度な長さに区切って複数ページに分割する機能です。
例えばモバイルアプリの場合、
サーバーに保存されているコンテンツをダウンロードして表示する場合、すべてのコンテンツを一度に全部ダウンロードすると要領が悪い。
複数ページに分割してダウンロードすることにより、効率的にアプリを動かすことができる。
実例
ページネーションの実例を示します。
以下は、Google ブックスアプリにおけるページネーションの実例です。
実装
今回は、Google Books APIs から取得した書籍情報をページネーションで表示する説明をします。
Google Books APIs
Google Books APIs とは、Google ブックスにある情報を取得できる WebAPI です。 以下に、WebAPIの呼び出し例とその結果を記載します。
呼び出し例
https://www.googleapis.com/books/v1/volumes?q=android&maxResults=30&startIndex=0
結果
{ "kind": "books#volumes", "totalItems": 1387, "items": [ { "kind": "books#volume", "id": "hkfWnQEACAAJ", "etag": "/u9dh+5dqEU", "selfLink": "https://www.googleapis.com/books/v1/volumes/hkfWnQEACAAJ", "volumeInfo": { "title": "Andoroido no nakami", "subtitle": "", "authors": [ "Tae Yeon Kim", "Hyung Joo Song", "Hoon Park Ji" ], "publishedDate": "2013-12", "description": "Androidの内部構造を徹底解剖。ソースコードの分析を通してAndroidフレームワークの全貌に迫る。", // 省略 }, // 省略 } ] }
説明
URL:https://www.googleapis.com/books/v1/volumes
No. | クエリ | 説明 |
---|---|---|
1 | q | 検索語句 |
2 | maxResults | 1 ページあたりの最大件数 |
3 | startIndex | startIndex:検索結果の開始位置(startIndex=0,1,2,3,…) |
以上より、リストを一番下までスクロールするたびに、startIndex を +1 して API を呼び出せばよいことがわかります。
Paging ライブラリ
Android には、Paging というページネーションの実装を簡略化するライブラリが存在するので、それを使用します。
概念図
以下に、Android の Paging ライブラリの概念図を示します。
build.gradle
以下のライブラリを追加します。
dependencies { // Paging ライブラリ implementation "androidx.paging:paging-runtime:3.2.0" implementation "androidx.paging:paging-rxjava3:3.2.0" implementation "io.reactivex.rxjava3:rxjava:3.1.6" implementation "io.reactivex.rxjava3:rxandroid:3.0.2" // リスト表示 implementation "androidx.recyclerview:recyclerview:1.3.1" // HTTP クライアント implementation "com.squareup.retrofit2:retrofit:2.9.0" implementation "com.squareup.retrofit2:adapter-rxjava3:2.9.0" implementation "com.squareup.retrofit2:converter-moshi:2.9.0" implementation "com.squareup.moshi:moshi:1.15.0" }
HTTP クライアント
以下の通り、HTTP クライアントを宣言します。
public interface GoogleBooksApisService { /** * 書籍を検索する。 * * @param query 検索語句 * @param maxResults 1ページあたりの最大件数 * @param startIndex 検索結果の開始位置 */ @GET("books/v1/volumes") @NonNull Single<Books> searchBooks(@Query("q") @NonNull String query, @Query("maxResults") int maxResults, @Query("startIndex") int startIndex); }
PagingSource
実際に WebAPI を呼び出す Paging ライブラリのクラスです。 PagingDataAdapter から要求されるたびに、インデックスを +1 加算して WebAPI を呼び出し、その結果を返します。
実装では、RxPagingSource を継承して、loadSingle をオーバーライドします。
public final class GoogleBooksPagingSource extends RxPagingSource<Integer, Item> { @NonNull @Override public Single<LoadResult<Integer, Item>> loadSingle( @NonNull final LoadParams<Integer> loadParams ) { // リストが一番下までスクロールされたときなどに呼び出される。 // WebAPIの呼び出しを実装する。 } }
RxPagingSource#loadSingle
WebAPI の呼び出しを実装します。
@NonNull @Override public Single<LoadResult<Integer, Item>> loadSingle( @NonNull final LoadParams<Integer> loadParams ) { // LoadParams#getKey()から次のインデックスを取得できる。 final Integer key = loadParams.getKey(); final int startIndex; // 次のインデックスがnullの場合、先頭番号0で初期化する。 if (key == null) { startIndex = 0; } else { startIndex = key; } // WebAPIを呼び出す return service.searchBooks(query, 20, startIndex) // ワーカースレッドで呼び出す。 .subscribeOn(Schedulers.io()) // 応答から必要な結果を取り出す。 .map((books) -> toLoadResult(books, startIndex)) // エラーハンドリング .onErrorReturn(LoadResult.Error::new); }
GoogleBooksPagingSource#toLoadResult
次のインデックスの加算と、WebAPI の応答からの必要な結果の取り出しを行います。
@NonNull private LoadResult<Integer, Item> toLoadResult(@NonNull final Books books, final int startIndex) { final List<Item> items = books.getItems(); final Integer nextKey; // 次のインデックスを決定する。 if (items.isEmpty()) { // データが空の場合には、nullにする。 nextKey = null; } else { // データが空でない場合には、インデックスを+1加算する。 nextKey = startIndex + 1; } // 読み取り結果を返す。 return new LoadResult.Page<>( items, null, nextKey, // 読み取り結果に次のインデックスを格納する。 LoadResult.Page.COUNT_UNDEFINED, LoadResult.Page.COUNT_UNDEFINED ); }
PagingDataAdapter
UI のリスト表示とデータをつなぐ Paging ライブラリのクラスです。 リストが一番下までスクロールされたことを検出して、新しいデータ要求します。 また、WebAPI 呼び出しの状態(読み取り中や読み取りエラーなど)を保持しているため、その状態を取得して、UI にローディング画面やエラー画面を表示することができます。
実装では、PagingDataAdapter を継承して、必要なメソッドをオーバーライドします。
public final class GoogleBooksAdapter extends PagingDataAdapter<Item, GoogleBooksAdapter.ViewHolder> { @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) { // ViewHolderを生成する。 } @Override public void onBindViewHolder(@NonNull final ViewHolder holder, final int position) { // ViewHolderにデータをバインドする。 } }