コンバンハ、saikiです。
こちらはAndroid Advent Calendar 2018、12日目の記事となります!メリクリ!間に合ってよかった。本当に。
さて、AACのPagingLibrary、皆さん使ってますか?
これからページングを実装するなら当然PagingLibrary使うでしょ!っていう雰囲気があると思うんですが、使おうと思うと
DataSource,DataSourceFactory,PagedList,PagedListAdapter,BoundaryCallback
となかなかに登場人物も多いので、ただ単にページングしたいだけなら自力で実装する方が単純っちゃあ単純ですよね。
と言うことで本当にPagingLibraryを使うべきなのか、なぜ使うのかを今更感はありますが調べてみました。
調べてて思ったんですが、Paging、他のAACと比べて言及されてる記事が少ない上に名前が一般的過ぎて情報集め辛くないですか…?
まあそれはさておき、幸いなことにCodelabがめちゃくちゃ良かったので、そちらをベースに進めていきます。
本記事ではCodelabの通り、APIからデータを取得しキャッシュとしてDB(Room)に保存する前提で話をしますのでよろしくお願いします。この前提は大事です。
実装方法を知りたいんじゃ!と言う方は直接CodeLabをやるのが一番いいのですが、英語でとっつきにくいし途中のコードがないのも寂しかったので、私がフォークして進めて要点だけ適当に和訳したリポジトリを上げておきましたので、もし良かったらみてみてください。
PagingLibraryとは?
Android Architecture Componentsの一つで、その名の通りページングを実現するためのライブラリです。
ページングを比較的簡単にいい感じに実装することが可能です。どのようにいい感じかはこの記事で説明します。
なぜPagingLibraryが必要か
さて、いままでは自力で実装されていたページング、わざわざその名を冠した専用のライブラリが作られ、使われるようになったのには当然理由がありそうです。
Codelabの「Githubリポジトリを検索してリスト表示するアプリ」を参考に、使わない実装の問題点と使うことでどう解決されるかを見ていきましょう。
時間がない人用に結論だけ言うと下記三つの問題点が解決されます。
- 自力で追加ロードの実装をしなければならない。
- データベースから全てのデータが一度にロードされてしまう(表示する分だけロードしたい)
- データベースから読み込んだ全てのデータのリストがメモリにのってしまう。
1個目はまあ当然なので大事なのは下二つです。
詳しく見ていきましょう。
PagingLibraryを使わない実装
CodelabのmasterブランチがPagingLibraryを使わない実装になっています。
APIとDB(Room)を併用しているので少し複雑ですが、処理の流れをざっくり説明すると、
検索した場合
検索実行
↓
DBからデータを 全て 引いてきてリスト表示
↓
同時にAPIリクエスト
↓
返ってきたらDBにinsert
↓
Roomを使っているのでDBに更新があれば自動でDBからデータが 全て 引かれて来てリスト表示
スクロールした場合
スクロール
↓
リスナーで検知
↓
持っているデータの末端まで行ったらAPIリクエスト
(末端まで行ったかどうかは自力でチェック)
↓
返ってきたらDBにinsert
↓
Roomを使っているのでDBに更新があれば自動でDBからデータが 全て 引かれて来てリスト表示
となっています。
Roomを使っていることにより、DBへ更新が来るのと同時にLiveDataに通知が来てリストが自動で更新されるのですが、Daoに記述されているデータ取得のクエリは以下ですから
1 2 3 |
@Query("SELECT * FROM repos WHERE (name LIKE :queryString) OR (description LIKE " + ":queryString) ORDER BY stars DESC, name ASC") fun reposByName(queryString: String): LiveData<List<Repo>> |
当然、DBに該当のデータが10000件あれば10000件取って来ることになります。
そして、その結果はRepoのListに変換されLiveDataにセットされるので、メモリに10000件のデータを持つことになります。
これが前述した
- データベースから全てのデータが一度にロードされてしまう。
- データベースから読み込んだ全てのデータのリストがメモリにのってしまう。
ですね。
自力で解決?
自力で解決するにはどうすればいいんだろうかと考えてみたんですが、私にはRoomの仕組みにのったまま解決できる気がしませんでした。
とにかくしんどそう。
もしできる方がいれば是非教えていただけると嬉しいです。
ということでPagingの登場です。
Pagingを使った実装
CodelabのsolutionブランチがPagingを使った実装となっています。
具体的な使い方や詳細な説明はCodelabに書いてあるので割愛しますが、データ取得のメソッドの返り値がLiveDataからDataSource.Factoryに変わり、LivePagedListBuilderなどを使ってごにょごにょすることで、最終的にViewがObserveするLiveDataのもつListがPagedListに変わります。
1 2 3 |
@Query("SELECT * FROM repos WHERE (name LIKE :queryString) OR (description LIKE " + ":queryString) ORDER BY stars DESC, name ASC") fun reposByName(queryString: String): DataSource.Factory<Int,Repo>//LiveData<List<Repo>>だった |
1 2 |
//val repos: LiveData<List<Repo>>だったのが下のようになる val repos: LiveData<PagedList<Repo>> |
これらの変更を加えると、必要な分しかDBから読み込まずメモリにものらないよううまいことやってくれるわけです。
1 2 3 4 |
// Get the paged list val data = LivePagedListBuilder(dataSourceFactory, DATABASE_PAGE_SIZE/*この分だけしか一度に読み込まないらしい*/) .setBoundaryCallback(boundaryCallback) .build() |
かしこい!
試しにActivityに流れて来るPagedListの中身を見て見ました。

このように、Sizeは全データ分ですが、中身は一部分のみしか入っておらず、必要のないところはnullになっているようです。
一度にどれくらい読み込むか、どれくらいPagedListに持たせておくかなどはLivePagedListBuilderにPagedList.Configを渡すことで色々と指定できそうです。(どの値がなんなのかまだ理解しきれていませんが)
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// 値はめちゃくちゃ適当なので気をつけてください! val conf = PagedList.Config.Builder() .setPrefetchDistance(20) .setPageSize(DATABASE_PAGE_SIZE) .setEnablePlaceholders(true) .setInitialLoadSizeHint(10) .setMaxSize(100) .build() // Get the paged list val data = LivePagedListBuilder(dataSourceFactory, conf/*ここで渡す*/) .setBoundaryCallback(boundaryCallback) .build() |
あとページングのライブラリなので当然っちゃ当然なのですが自力での追加読み込みの実装をしなくて済みます。
Activityでこんな感じリスナーをセットしてviewModelで閾値チェック&リクエストしてたのをまるっと消せます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
private fun setupScrollListener() { val layoutManager = list.layoutManager as androidx.recyclerview.widget.LinearLayoutManager list.addOnScrollListener(object : androidx.recyclerview.widget.RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: androidx.recyclerview.widget.RecyclerView, dx: Int, dy: Int) { super.onScrolled(recyclerView, dx, dy) val totalItemCount = layoutManager.itemCount val visibleItemCount = layoutManager.childCount val lastVisibleItem = layoutManager.findLastVisibleItemPosition() viewModel.listScrolled(visibleItemCount, lastVisibleItem, totalItemCount) } }) } |
その代わりにBoundaryCallbackを継承したクラスを作り中に、DBが空だった時の動作と、最後までスクロールした時の動作を書いてやれば良いわけです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
/** * Database returned 0 items. We should query the backend for more items. */ override fun onZeroItemsLoaded() { Log.d("RepoBoundaryCallback", "onZeroItemsLoaded") requestAndSaveData(query) } /** * When all items in the database were loaded, we need to query the backend for more items. */ override fun onItemAtEndLoaded(itemAtEnd: Repo) { Log.d("RepoBoundaryCallback", "onItemAtEndLoaded") requestAndSaveData(query) } |
シンプルですね。
ちなみに、PagingLibraryはRoomを使わず、APIからのデータのみでも実装可能です。
が、その場合表示されているアイテムのデータを一つだけ更新するようなことができないうえに、おそらくDBで持っていたキャッシュをどうしてもメモリに持つことになりそうですよね。
ちょっと不便な上にPagingLibraryの恩恵を受けきれないので、基本的にはRoomを使う形で考えるのが良さそうです。
まとめ
と言うことで、AndroidArchitectureComponentのPagingLibralyは自力でDB(Room)+APIを利用したページングを実装した時に起こる
- 自力で追加ロードの実装をしなければならない。
- データベースから全てのデータが一度にロードされてしまう。
- データベースから読み込んだ全てのデータのリストがメモリにのってしまう。
という問題を簡単に解決するぞ!という気持ちで使うと良さそうでした。
内部の仕組みはまだまだ雰囲気しかわかっていませんが、少なくともなんのために使うのかがわかってよかったです。
実装方法に関してはCodeLabを進めればわかるので是非みなさんやって見てください。
次はListAdapterについて書きたいと思っています。
ではまた。