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

## こんな経験ないですか？

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

## ページネーションって何？

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

## `Paging 3`

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

## 登場人物は以下の通り

### リポジトリレイヤ

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

### ViewModelレイヤ

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

### UIレイヤ

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

## データの流れ

`APIデータ`→`PagingSource`→`Pager`→`Flow<PagingData>`→`LazyPagingItems`

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2989129/d54d4f34-8d78-4c26-9991-a3ec58504da6.png)

## それぞれの役割

## `APIデータ`→`PagingSource`

### データソースを定義

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

`getRefreshKey`と`load`を実装しなければならない

`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を足した値を設定する

```kotlin
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)
        }
    }
}
```

```kotlin
class CharacterRepositoryImpl @Inject constructor(
    private val ktorClient: KtorClient,
) : CharacterRepository {
    override fun getCharacterPagingSource(): PagingSource<Int, Character> {
        return CharacterPagingSource(ktorClient)
    }
}
```

## `PagingSource`→`Pager`→`Flow<PagingData>`

**ポイント**

- `config` でページングの設定を行う
- `pagingSourceFactory` でデータソースを管理
- `Pager`の`.flow`プロパティを呼び出すと`Flow<PagingData>`というデータストリームが生成される
- `.cachedIn(viewModelScope)` で画面回転などの設定変更時にデータの再ロードを防ぐ

```kotlin
@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`

```kotlin
@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()` に変更

```kotlin
@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](https://developer.android.com/topic/libraries/architecture/paging/v3-overview?hl=ja)

[Jetpack ComposeとPaging3で実現する Endless Grid Design](https://speakerdeck.com/akkie76/jetpack-composetopaging3deshi-xian-suru-endless-grid-design)

[【Android】Paging 3で実装するEndless Grid Design - Qiita](https://qiita.com/akkie76/items/8df6d78e7abba8b7b146)

[ページング データを読み込んで表示する  |  Android Developers](https://developer.android.com/topic/libraries/architecture/paging/v3-paged-data?_gl=1*1a7uyyn*_up*MQ..*_ga*NTUzNjYyOTI4LjE3NDE3NDg2NjA.*_ga_6HH9YJMN9M*MTc0MTc0ODY2MC4xLjAuMTc0MTc0ODY2MC4wLjAuMTAyMjQ5MzQ0Ng..&hl=ja)

[Paging3をざっくり理解したい - Qiita](https://qiita.com/kitakkun/items/1cd18aabdc567141e0d1)

[Android における Paging 3 を用いたページングの導入 - Pepabo Tech Portal](https://tech.pepabo.com/2021/10/18/android-paging3/)