4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ラクスAdvent Calendar 2023

Day 24

【Android】Paging 3で実装するEndless Grid Design

Last updated at Posted at 2023-12-23

この記事は、ラクスアドベントカレンダー 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 のフローから取得したデータを表示するためのクラス

今回実装するサンプルアプリ

では、実際に以下のようなグリッドデザインのページングデザインのページを実装して行きます。

screenshot.gif

実装にあたり使用しているライブラリは、以下を使用しています。

  • 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 用の画像読み込みライブラリです。

アーキテクチャ

スクリーンショット 2023-12-21 21.39.55.png

アーキテクチャは、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 アプリ開発において頻繁に利用される機能であり、本記事がその基本的な理解と実装に役立てば幸いです。

ページングを導入することで、アプリのパフォーマンス向上やユーザビリティの向上に寄与することができるでしょう。是非、実際のプロジェクトにおいても活用してみてください。

4
1
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
4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?