Jetpack Compose で追加読み込みに対応した LazyColumn を実装する

Jetpack Compose で追加読み込みに対応した`LazyColumn`を実装しましたので、その方法をまとめます。

具体的には、一番下までスクロールしたときに追加読み込みを行う処理を呼び出すところまでの実装になります。

# はじめに

まず、Jetpack Compose には[Lazy コンポーザブル](https://developer.android.com/jetpack/compose/lists?hl=ja#lazy)という Composable があり、RecyclerView と同様の役割を担います。

Lazy コンポーザブルには`LazyColumn`や`LazyRow`などがあります。本記事では`LazyColumn`を使用します。

# 実装コード

以下に実装コードを示します（ご指摘いただきました@Nabe1216 さん、ありがとうございました）。

```kotlin

/**
 * 追加読み込みに対応したリスト
 * @param items 表示したいアイテム
 * @param onAppearLastItem リストの一番最後に到達した際に呼ばれる関数
 */
@Composable
fun AdditionalReadableItems(items: List<String>, onAppearLastItem: (Int) -> Unit) {
    val listState = rememberLazyListState().apply {
        OnAppearLastItem(onAppearLastItem = onAppearLastItem)
    }
    LazyColumn(state = listState) {
        items(items) {
            Text(text = it)
            Divider(color = Color.Gray)
        }
    }
}

/**
 * リストの一番最後に到達した際、追加読み込みなどを行うための処理を発火する
 * @param onAppearLastItem リストの一番最後に到達した際に呼ばれる関数
 */
@Composable
fun LazyListState.OnAppearLastItem(onAppearLastItem: (Int) -> Unit) {
    val isReachedToListEnd by remember {
        derivedStateOf {
            // 追加読み込みを行う条件
            // 1. アイテム総数が画面に収まりきらないほどある
            // 2. リストの下端までスクロールされた
            layoutInfo.visibleItemsInfo.size < layoutInfo.totalItemsCount &&
                layoutInfo.visibleItemsInfo.lastOrNull()?.index == layoutInfo.totalItemsCount - 1
        }
    }

    LaunchedEffect(Unit) {
        snapshotFlow { isReachedToListEnd }
            .filter { it }
            .collect {
                onAppearLastItem(layoutInfo.totalItemsCount)
            }
    }
}
```

関数ごとにそれぞれ解説します。

# 解説

## `AdditionalReadableItems`（UI）部分

まず、`rememberLazyListState()`でリストの状態を取得しておきます。

`OnAppearLastItem(onAppearLastItem = onAppearLastItem)`については後で説明します。

```kotlin
val listState = rememberLazyListState().apply {
    OnAppearLastItem(onAppearLastItem = onAppearLastItem)
}
```

次に、リスト部分の UI を作成します。

本記事ではここはあまり重要でないため、簡易的にしています。

```kotlin
LazyColumn(state = listState) {
    items(items) {
        Text(text = it)
        Divider(color = Color.Gray)
    }
    ...
}
```

ここで、`state = listState`としておくことでリストの状態がリアルタイムで反映されるようになります。

## `LazyListState.OnAppearLastItem`（ロジック）部分

まず、リストの一番最後に到達したかどうかを判定する処理を作成します。

処理については`LazyListState`の拡張関数として作成することで、他の Composable でも使いまわせるようにしておくと便利です。

```kotlin
val isReachedToListEnd by remember {
    derivedStateOf {
        // 追加読み込みを行う条件
        // 1. アイテム総数が画面に収まりきらないほどある
        // 2. リストの下端までスクロールされた
        layoutInfo.visibleItemsInfo.size < layoutInfo.totalItemsCount &&
            layoutInfo.visibleItemsInfo.lastOrNull()?.index == layoutInfo.totalItemsCount - 1
    }
}
```

`listState.layoutInfo.visibleItemsInfo.size`は現在表示中のアイテムの数です。`listState.layoutInfo.totalItemsCount`はアイテムの総数です。

現在表示中のアイテムの数がアイテムの総数より少ないか（アイテム総数が画面に収まりきらないほどあるか）をチェックすることで、何もしていないのに追加読み込みが行われてしまうのを防ぎます。

`layoutInfo.visibleItemsInfo.lastOrNull()?.index`は現在表示中のアイテムの一番最後のインデックスです。これとアイテムの総数を比較し、リストの下端までスクロールされたかをチェックします。

また、`derivedStateOf`を使用することで、状態が変化した時のみ計算されるようにして無駄に再コンポーズされないようにします。

次に、リストの一番最後に到達した時のみ`onAppearLastItem`を発火する処理を作成します。

```kotlin
LaunchedEffect(Unit) {
    snapshotFlow { isReachedToListEnd }
        .filter { it }
        .collect {
            onAppearLastItem(layoutInfo.totalItemsCount)
        }
}
```

`LaunchedEffect`は Composable が表示されるタイミングで何か処理を実行したい時に使用します。引数に`Unit`のような定数を渡すことで、ライフサイクルと一致する作用を作成できます。

`isReachedToListEnd`にはリストの一番最後に到達したかどうかが入っているので、それをコールド Flow に変換して流し、`true`だったら`onAppearLastItem`を実行します。

また、下記のエラーが出る場合は`kotlinx.coroutines.flow.collect`が import されているか確認してください。

```
Type mismatch.
Required: FlowCollector<Boolean>
Found: () → Unit
```

これで追加読み込みに対応した`LazyColumn`を実装できました。

# まとめ

Jetpack Compose で追加読み込みに対応した`LazyColumn`を実装する方法をまとめました。

Jetpack Compose は 2021 年 11 月 1 日時点ではまだ情報が少なく、複雑な実装をしようとすると手探りの状態になることが多いです。

同じような実装をしようとしていてうまくいかない人への助けとなれば幸いです。

# 参考URL
- [android - Get last visible item index in jetpack compose LazyColumn - Stack Overflow](https://stackoverflow.com/questions/66712286/get-last-visible-item-index-in-jetpack-compose-lazycolumn)
- [Jetpack Composeでリストの最後を検知する - Qiita](https://qiita.com/b4tchkn/items/30440b37efcad28f7184)
- [JetpackComposeの無限リスト/ページリスト](https://ichi.pro/jetpackcompose-no-mugen-risuto-pe-ji-risuto-194681336448872)
- [Compose における副作用  |  Jetpack Compose  |  Android Developers](https://developer.android.com/jetpack/compose/side-effects?hl=ja)
- [Jetpack ComposeのLaunchedEffectについて - Qiita](https://qiita.com/Nabe1216/items/4b4adbac382783be3442)
- [android - Type mismatch inferred type is () -> Unit but FlowCollector<Int> was expected - Stack Overflow](https://stackoverflow.com/questions/62347539/type-mismatch-inferred-type-is-unit-but-flowcollectorint-was-expected)