0
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?

【Android】Paging3を用いてページングを実装してみた

Posted at

初めに

本記事では、Paging3というライブラリを用いたページング処理の実装について自分の理解をまとめておこうと思います。

ページングとは

ページングは大量のデータを少しづつ読み込むことでアプリのパフォーマンスを向上させる仕組みです。

ページングのメリット

ページング導入のメリットとして、UXの向上が挙げられます。APIを叩いてのデータ取得や、ローカルに保存したデータの取得を行う際に、大量のデータを一度にすべて読み込もうとするとユーザーを長時間待たせることになります。しかし、Pagingを用いるとすぐに必要になる(=画面に表示する必要のある)データのみを取得するため、アプリの応答速度を改善することができます。

Paging3に関するクラス

Paging3における主な構成要素となるクラスについて、以下に簡単な概要をまとめておきます。

名前 概要・用途
PagingSource データソースからのデータの取得方法を定義するクラス
PagingData ページングによってロードするデータのコンテナとなるクラス
Pager PagingSourceからデータを取得してPagingDataを作成するクラス
PagingConfig PagingSourceで定義した方法でデータを取得する際の設定をするクラス
LazyPagingItems PagingDataにアクセスし、UIに表示するために使用するクラス

ページングの実装

今回は、RetrofitでJSONPlaceholderというモックAPIを叩いて情報を取得し、それをUI側で表示するというケースを例にして実装を行います。
JSONPlaceholderの詳細

PagingSourceの実装

class PostPagingSource(
    private val apiService: APIService
) : PagingSource<Int, Post>() {

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Post> {
        return try {
            // 次にロードするページのキー
            val page = params.key ?: 1
            // 1ページあたりのアイテム数
            val limit = params.loadSize
            
            val response = apiService.getPosts(page = page, limit = limit)
            LoadResult.Page(
                data = response,
                prevKey = if (page == 1) null else page - 1,
                nextKey = if (response.isEmpty()) null else page + 1
            )
        } catch (e: Exception) {
            LoadResult.Error(e)
        }
    }

    override fun getRefreshKey(state: PagingState<Int, Post>): Int? {
        return state.anchorPosition?.let { anchorPosition ->
            state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
                ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
        }
    }
}

PagingSourceの拡張クラスにおいては、loadメソッドgetRefreshKeyメソッドの2つをオーバーライドする必要があります。

loadメソッド

loadメソッドはページングデータのロードを行うためのメソッドです。
引数として、LoadParamsオブジェクトを受け取りますが、これはメンバとしてkey(現在のページ番号)とloadSize(1回のリクエストで取得するアイテムの数)を持っています。なお、loadSizeは後述するpagingConfigによって決定されます。

オーバーライドしたloadメソッド内では、まず次にロードするページのキーと1ページ当たりのアイテム数を取得し、これをもとに非同期にAPIを叩いてデータをロードしています。

なお、今回APIを叩くときのリクエストは以下のようになっています。(例:page=1,limit=10のとき)

https://jsonplaceholder.typicode.com/posts?_page=1&_limit=10

このとき、1ページ目の最初の10件のデータを取得することができます。

なお、ロードした結果はLoadResultオブジェクトで返されます。これは、データの取得に成功したらLoadResult.Pageを、失敗したらLoadResult.Errorを返します。LoadResult.Pageは以下の3つのメンバが含まれます。

  • data:取得したアイテムのリスト
  • prevKey:現在のページの1つ前のページをロードするときに使用するキー
  • nextKey:現在のページの1つ後のページをロードするときに使用するキー

上記のコードでは、APIを叩いて取得したresponseをdataに格納し、ページが先頭だったときはprevKeyを、ページが最後尾だったときはnextKeyをnullにしています。

getRefreshKeyメソッド

PagingSourceの拡張クラスは、これに加えてgetRefreshKeyメソッドもオーバーライドします。引数として、PagingStateオブジェクトを受け取りますが、これはページングの現在の状態を管理するオブジェクトです。このメソッドでは、PagingStateの持つ情報をもとにしてどのページをリロードするのかを決定します。
getRefreshKeyメソッドを呼び出す必要のある状況としては、以下のようなものが挙げられます。

  • ユーザーが手動でリフレッシュ(再読み込み)した場合
  • 初回のデータ読み込みのとき
  • Roomデータベースが変更された場合(PagingSourceがローカルのDBであるケース)

このような状況では、現在表示されている位置を考慮して適切なページを再取得する必要があります。
上記のコードでは、PagingStateが持つanchorPosition(現在のスクロール位置)の情報をもとにそれに最も近いページ情報をclosestPagetoPositionから取得、そしてそのページを基準としてprevKey,nextKeyを更新します。

ViewModelの作成

class PostsViewModel() : ViewModel() {
    private val pagingConfig = PagingConfig(
        pageSize = 5, // 1回のデータ取得で取得するアイテム数
        enablePlaceholders = false, // プレースホルダーを無効にする
        initialLoadSize = 10, // 最初の読み込みで取得するデータ数
        prefetchDistance = 5 // あらかじめデータを読み込む距離
    )

    val items: Flow<PagingData<Post>> = Pager(
        config = pagingConfig,
        pagingSourceFactory = { PostPagingSource(Retrofit.api) }
    ).flow.cachedIn(viewModelScope) //画面回転などでもデータを保持
}

PagingConfigでは、ページングの動作を制御するための各種設定を行っています。
上記のコードにおいては、pageSizeで1回のデータ取得で取得するアイテム数を、initialloadsizeで初回の読み込みで取得するデータ数を指定します。prefetchDistanceではユーザーのスクロールで次のページまでのアイテム数が5件になったらあらかじめ次のページをダウンロードするという設定にしています。

また、そのPagingConfigでの設定に従ってPagerPagingSourceからロードしたデータをitemsに格納しています。このitemsの型はFlow<PagingData<Post>>となっています。これにより、非同期にデータの更新・取得を行い、UI側に反映することができます。

UI上での表示

@Composable
fun PostsScreen(viewmodel: PostsViewModel) {
    val posts = viewmodel.items.collectAsLazyPagingItems()

    LazyColumn(
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        items(posts.itemCount) { index ->
            val post = posts[index]
            PostCard(
                post = post!!
            )
        }
        posts.apply {
            when {
                loadState.refresh is LoadState.Loading -> {
                    item { CircularProgressIndicator() }
                }

                loadState.append is LoadState.Loading -> {
                    item { CircularProgressIndicator() }
                }

                loadState.append is LoadState.Error -> {
                    item {
                        Text(text = "Error loading data")
                    }
                }
            }
        }
    }
}


@Composable
fun PostCard(
    post: Post
) {
    Card(
        modifier = Modifier
            .padding(16.dp)
            .fillMaxWidth()
    ) {
        Column {
            Text(
                text = "No.${post.id}",
                modifier = Modifier.padding(8.dp),
            )
            Text(
                text = "タイトル\n${post.title}",
                modifier = Modifier.padding(8.dp),
            )
            Text(
                text = "内容\n${post.body}",
                modifier = Modifier.padding(8.dp),
            )
        }
    }
}

viewmodel.itemsFlow<PagingData<Post>> なので、これをcollectAsLazyPagingItems() で UI 側で利用できる形に変換し、LazyColumnで縦に並べています。

動作例

上記のコードでの動作例は以下の通りです。
無題の動画 ‐ Clipchampで作成.gif
数十件のアイテムの情報を取得しているものの、データ取得により発生する待ち時間がほとんどないことが分かります。

最後に

本記事ではPaging3を用いたページングの実装方法についてまとめました。
Android開発初学者ですので、間違い等ありましたら指摘していただけると助かります。

0
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
0
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?