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?

ページネーションってなんだ

Last updated at Posted at 2025-04-05

こんな経験ないですか?

  • 大規模なデータのリストを表示したいけどやり方がわからない
    • すべて読みこんでからリスト表示すると非効率そう

ページネーションって何?

これらの課題を解決する技術がページネーション

Paging 3

Androidでの実装はPaging3ライブラリを用いる

登場人物は以下の通り

リポジトリレイヤ

  • PagingSource
    • 特定のページクエリのデータチャンクを読み込むためのクラス

ViewModelレイヤ

  • Pager
    • PagingSourceからページングされたデータのストリーム(Flow<PagingData>)を生成する
  • Flow<PagingData>
    • ページングされたデータの非同期ストリーム。データのロード状態、エラー、およびページネーション情報をUIに提供する。

UIレイヤ

  • LazyPagingItems
    • PagingDataのフローから取得したデータを表示するためのクラス

データの流れ

APIデータPagingSourcePagerFlow<PagingData>LazyPagingItems

image.png

それぞれの役割

APIデータPagingSource

データソースを定義

API通信したpage番号ごとにアイテムのページを読み込む実装を行う
kotlinコルーチンを使用するにはPagingSourceクラスを直接使う
返り値は PagingSource<Int, HogeDataClass>()

getRefreshKeyloadを実装しなければならない

getRefreshKey

リフレッシュ(データの再読み込み)を行う際に、どのキー(ページ番号)からリフレッシュを開始するかを決定するために使用される

load

Web API やローカルストレージからデータを取得し、LoadResult 型で返す

key

  • prevKey:
    • 現在のページよりも前のページのキーを表す
    • if (page == 1) null else page - 1 という条件式で計算される
      • これは、最初のページ(ページ番号1)の場合、前のページが存在しないため null を設定し、それ以外のページでは現在のページ番号から1を引いた値を設定する
  • nextKey:
    • 現在のページよりも後のページのキーを表す
    • if (characterPage.characters.isEmpty()) null else page + 1 という条件式で計算される
      • これは、現在のページのキャラクターリストが空の場合、次のページが存在しないため null を設定し、それ以外の場合は現在のページ番号に1を足した値を設定する
class CharacterPagingSource(private val ktorClient: KtorClient) :
    PagingSource<Int, Character>() {
    override fun getRefreshKey(state: PagingState<Int, Character>): Int? {
        // アンカー位置(最後にアクセスされたアイテムの位置)から最も近いページ番号を返す
        return state.anchorPosition?.let { anchorPosition ->
            state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
                ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
        }
    }
    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Character> {
        // 最初のロードは1ページ目がロードされる
        val page = params.key ?: 1
        return try {
            when (val result = ktorClient.getCharacterByPage(page)) {
                is ApiOperation.Success -> {
                    val characterPage = result.data
                    LoadResult.Page(
                        data = characterPage.characters,
                        prevKey = if (page == 1) null else page - 1,
                        nextKey = if (characterPage.characters.isEmpty()) null else page + 1
                    )
                }
                is ApiOperation.Failure -> LoadResult.Error(result.exception)
            }
        } catch (e: Exception) {
            LoadResult.Error(e)
        }
    }
}
class CharacterRepositoryImpl @Inject constructor(
    private val ktorClient: KtorClient,
) : CharacterRepository {
    override fun getCharacterPagingSource(): PagingSource<Int, Character> {
        return CharacterPagingSource(ktorClient)
    }
}

PagingSourcePagerFlow<PagingData>

ポイント

  • config でページングの設定を行う
  • pagingSourceFactory でデータソースを管理
  • Pager.flowプロパティを呼び出すとFlow<PagingData>というデータストリームが生成される
  • .cachedIn(viewModelScope) で画面回転などの設定変更時にデータの再ロードを防ぐ
@HiltViewModel
class HomeScreenViewModel @Inject constructor(
    private val repository: CharacterRepository,
) : ViewModel() {
    private val _viewState = MutableStateFlow<HomeScreenViewState>(HomeScreenViewState.Loading)
    val viewState: StateFlow<HomeScreenViewState> = _viewState.asStateFlow()

    init {
        loadCharacters()
    }

    private fun loadCharacters() {
        viewModelScope.launch {
            try {
                val pagingSource = repository.getCharacterPagingSource()
                _viewState.update {
                    HomeScreenViewState.GridDisplay(
                        characters = Pager(
                            config = PagingConfig(enablePlaceholders = false, pageSize = 20),
                            pagingSourceFactory = { pagingSource }
                        ).flow.cachedIn(viewModelScope)
                    )
                }
            } catch (e: Exception) {
                _viewState.update { HomeScreenViewState.Error(e.message ?: "Unknown error") }
            }
        }
    }
}

Flow<PagingData>LazyPagingItems

@Composable
fun HomeScreen(
    viewModel: HomeScreenViewModel = hiltViewModel(),
) {
    val viewState by viewModel.viewState.collectAsStateWithLifecycle()

    when (val state = viewState) {
        is HomeScreenViewState.Loading -> {
            LoadingState()
        }

        is HomeScreenViewState.GridDisplay -> {
            Log.d("HomeScreen", "viewState in GridDisplay: $state")
            val characters = state.characters.collectAsLazyPagingItems()
            CharacterGrid(characters = characters)
        }

        is HomeScreenViewState.Error -> {
            val errorMessage = state.errorMessage
            Text(text = "Error: $errorMessage")
        }
    }
}

Flow<PagingData<Character>>.collectAsLazyPagingItems() に変更

@Composable
fun CharacterGrid(
    characters: LazyPagingItems<Character>,
) {
    val navController = LocalNavController.current
    LazyVerticalGrid(
        contentPadding = PaddingValues(all = 16.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp),
        horizontalArrangement = Arrangement.spacedBy(8.dp),
        columns = GridCells.Fixed(2),
    ) {
        items(characters.itemCount) { index ->
            val character = characters[index]
            character?.let {
                CharacterGridItem(
                    modifier = Modifier,
                    character = it,
                    onCharacterSelected = { navController.navigateToCharacterDetailsScreen(it.id) }
                )
            }
        }
    }
}

itemに流し込んで終了

参考文献

ページング ライブラリの概要  |  Android Developers

Jetpack ComposeとPaging3で実現する Endless Grid Design

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

ページング データを読み込んで表示する  |  Android Developers

Paging3をざっくり理解したい - Qiita

Android における Paging 3 を用いたページングの導入 - Pepabo Tech Portal

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?