この記事は、ラクスアドベントカレンダー 2023 の 24 日目の記事です 🎄
はじめに
最近チームの育成で使用するサンプルアプリを作成してますが、育成カリキュラムの中でページングの実装を取り入れようと検討しています。モバイル開発では、API からデータを取得し、リスト表示するようなユースケースも多いと思いますので、今回はAndroid の Paging 3 ライブラリの基本的な使い方を紹介したいと思います。
ページングライブラリの概要
ページングライブラリは、Android Jetpack で提供されるライブラリの 1 つです。現在のバージョンは3系なので、Paging 3 と呼ばれたりします。
さて、このページングライブラリですが、使用することで大規模なデータセットからローカルストレージやネットワーク経由でデータのページを読み込んで表示させることができます。これにより、アプリがネットワーク帯域幅とシステムリソースの両方をより効率的に使用できるようになることが期待されます。他にも、ページングライブラリのコンポーネントは、Android アプリアーキテクチャに適合すると同時に、Jetpack Compose との統合により、Kotlin によるサポートが十分提供されるように設計されています。
ページングライブラリの主な機能
ページングライブラリの主な機能としては、以下の5つの機能があります。
- ページングデータに対するメモリ内キャッシュ
- リクエスト重複排除
- 画面スクロールに連動した際に自動的なリクエスト
- ページングの状態管理
- Coroutine、Flow、LiveData、RxJava に対応
これらの機能により、リソースを効率化したページングデータの操作を実現します。
ページングライブラリの主コンポーネント
今日紹介するサンプルアプリでも登場しますが、ページングライブラリの実装に登場する主なコンポーネントについてまとめるとこのような感じになります。
クラス名 | 説明 |
---|---|
PagingData | ページングされたデータのコンテナ |
Pager | PagingData のリアクティブストリームの生成を行うクラス |
PagingConfig | ページング動作を決定するパラメータを定義するクラス |
PagingSource | 特定のページクエリのデータチャンクを読み込むためのクラス |
LazyPagingItems | PagingData のフローから取得したデータを表示するためのクラス |
今回実装するサンプルアプリ
では、実際に以下のようなグリッドデザインのページングデザインのページを実装して行きます。
実装にあたり使用しているライブラリは、以下を使用しています。
- paging 3
- paging-compose 3
- Coil
- Unsplash Image API
ページングの実装には、paging と paging-compose を使用していますが、画像情報の取得は Unsplash APIを使用しています。Unsplash APIは、ユーザーのためのあらゆる体験を構築するために必要なすべての情報を提供する現代的な JSON API です。非常に使いやすい API としてunsplash.com でも利用されています。
Coil は Kotlin Coroutines で作られた Android 用の画像読み込みライブラリです。
アーキテクチャ
アーキテクチャは、Android アプリ開発ではお馴染みの MVVM アーキテクチャです。Data 層の Repository から取得したデータをリアクティブに画面に反映させます。実装にあたり、依存関係の注入は Hilt を使用しています。
Repository
まずは、Repository の実装から見ていきます。
class UnsplashSearchRepositoryImpl @Inject constructor(
private val service: UnsplashService
): UnsplashSearchRepository {
override fun searchPhotos(query: String): Flow<PagingData<UnsplashPhoto>> {
return Pager(
config = PagingConfig(enablePlaceholders = false, pageSize = PAGE_SIZE),
pagingSourceFactory = { UnsplashPagingSource(service, query) }
).flow
}
companion object {
private const val PAGE_SIZE = 25
}
}
Repository ではFlow<PagingData<T>>
型の関数を公開しています。
PagingData は読み込んだデータをラップし、ページングライブラリが追加のデータを取得するタイミングを決定します。また、同じページを再リクエストしないようにしてくれます。
PagingConfig
PagingConfig には、先読みの量や初期読み込みのサイズ、リクエストなど、PagingSource からコンテンツを読み込む方法のオプションを設定します。
PagingSource
今回の実装では、PagingSource を継承した UnsplashPagingSource クラスを作成し、PagingSource の作成方法をページングライブラリに伝える関数として設定します。
class UnsplashPagingSource(
private val service: UnsplashService,
private val query: String
) : PagingSource<Int, UnsplashPhoto>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, UnsplashPhoto> {
val page = params.key ?: UNSPLASH_STARTING_PAGE_INDEX
return try {
val response = service.searchPhotos(query, page, params.loadSize)
val photos = response.results
LoadResult.Page(
data = photos,
prevKey = if (page == UNSPLASH_STARTING_PAGE_INDEX) null else page - 1,
nextKey = if (page == response.totalPages) null else page + 1
)
} catch (exception: Exception) {
LoadResult.Error(exception)
}
}
}
PagingSource では、load() と getRefreshKey() という2つの関数を実装する必要があります。
load()関数はページングライブラリによって呼び出され、ユーザーがスクロールしたときに表示される追加のデータを非同期で取得します。LoadParams オブジェクトは、読み込み操作に関する情報を保持します。
読み込みのページのキー (params.key)
load() が初めて呼び出された場合、LoadParams.key は null になります。そのため、今回は最初のページキーを定義しています。
読み込みサイズ (params.loadSize)
params.loadSize
は読み込むアイテムのリクエスト数です。ページが小さすぎると、ページのコンテンツが全画面に満たないため、リストがチラつく可能性があります。ページサイズを大きくすると、読み込みの効率は高くなりますが、リストが更新されたときの待ち時間が長くなる可能性があるので注意が必要です。
ViewModel
次は、ViewModel から PagingData を取得する実装を見ていきたいと思います。
@HiltViewModel
class MainViewModel @Inject constructor(
searchRepository: UnsplashSearchRepository
): ViewModel() {
val photos = searchRepository.searchPhotos(QUERY).cachedIn(viewModelScope)
companion object {
private const val QUERY = "fruit"
}
}
ViewModel では、PagingData をリクエストして cache するシンプルな実装になっています。リクエストを再度トリガーする必要がないよう、リクエスト後に終了演算子として cachedIn を行う必要があります。今回は検索クエリをfruit
固定にしていますが、検索フィールドを追加すれば、検索文字でリクエストするような実装も可能です。
Composable
キャッシュした画像を表示する実装は、以下になります。
@Composable
fun MainScreen(viewModel: MainViewModel) {
Scaffold { paddingValues ->
val lazyPagingItems = viewModel.photos.collectAsLazyPagingItems()
LazyVerticalGrid(
columns = GridCells.Adaptive(minSize = 128.dp),
modifier = Modifier.padding(paddingValues)
) {
items(
count = lazyPagingItems.itemCount,
key = lazyPagingItems.itemKey()
) { index ->
val photo = lazyPagingItems[index] ?: return@items
AsyncImage(
model = photo.urls.small,
contentDescription = null,
modifier = Modifier.fillMaxWidth().height(128.dp),
contentScale = ContentScale.FillWidth
)
}
}
}
}
データ表示を行う Composable では、ViewModel から公開された PagingDataのFlow データFlow<PagingData<T>>
を LazyPagingItems として保持します。今回のサンプルアプリでは、LazyVerticalGrid で LazyPagingItems の要素を表示しています。画像は Coil の AsyncImage を利用しており、model に URL を渡すだけで画像を読み込んで表示してくれます。
このようにページングライブラリを使うことで、この位のコード量でエンドレススクロールデザインを実装することができます。
ページングライブラリの状態管理
ページングライブラリの状態管理では、CombinedLoadStates 型で以下の読み込み状態にアクセスすることが可能です。
State | 説明 |
---|---|
LoadState.append | ユーザーの現在位置より後に取得されるアイテムの LoadState |
LoadState.prepend | ユーザーの現在位置より前に取得されるアイテムの LoadState |
LoadState.refresh | 初期読み込みの LoadState |
LoadState から取得できる状態としては、以下の 3 種類の状態を取得できます。これらの状態を組み合わせることで柔軟に状態管理による実装を実現することができます。NotLoading
については、初期状態も含まれるので使い方を工夫する必要がある場面もあるかと思います。
State | 説明 |
---|---|
Loading | アイテムを読み込み中 |
NotLoading | アイテムを読み込んでいない(初期状態も含まれる) |
Error | 読み込みエラーが発生 |
参考 : Manage and present loading states
読み込み中にCircularProgressIndicator
を表示させるような実装であれば、このように実装することで実現できます。
when (lazyPagingItems.loadState.refresh) {
is LoadState.Loading -> {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
Text(
modifier = Modifier.padding(8.dp),
text = "Loading"
)
CircularProgressIndicator(
modifier = Modifier.padding(top = 8.dp),
color = Color.LightGray
)
}
}
else -> {}
}
プレビューするとこのようになります。
まとめ
今回は、Android の Paging 3 ライブラリの基本的な使い方と、実際のサンプルアプリの実装方法について紹介しました。ページングライブラリを使用することで、大規模なデータセットを効率的に扱い、ユーザーエクスペリエンスを向上させることが期待されます。
ページングライブラリは、Android アプリ開発において頻繁に利用される機能であり、本記事がその基本的な理解と実装に役立てば幸いです。
ページングを導入することで、アプリのパフォーマンス向上やユーザビリティの向上に寄与することができるでしょう。是非、実際のプロジェクトにおいても活用してみてください。