Jetpack Composeで特定要素での遅延読み込みを実装する

## やりたいこと
LazyColumnで複数の要素を表示する際に、特定の要素が表示されたタイミングで追加のデータが読み込まれるようにします。これにより、初期表示を高速化したり不要なAPI呼び出しを制限することでパフォーマンスの向上を目指します。

## LazyColumnの実装
まず、LazyColumnで表示するデータの準備を行います。今回は色とりどりなコンテンツを表示するために、sealed classでdata objectを定義します。

```kotlin
sealed class ContentType {
    data object Header : ContentType()
    data object Red : ContentType()
    data object Yellow : ContentType()
    data object Blue : ContentType()
    data object Green : ContentType()
    data object Magenta : ContentType()
    data object Cyan : ContentType()
    data object LoadingIndicator : ContentType()
}
```
次に、表示するコンテンツを格納するためのmutableStateListを作成します。

```kotlin
val contentList = remember {
    mutableStateListOf(
        ContentType.Header,
        ContentType.Red,
        ContentType.Yellow,
        ContentType.Blue,
        ContentType.Green
    )
}
```
CntentTypeに対応したComposableも実装して、以下のように分岐してそれぞれのComposableが表示されるようなLazyColumnを作成します。

```kotlin
LazyColumn(
    modifier = Modifier
        .fillMaxWidth()
        .fillMaxHeight(),
    verticalArrangement = Arrangement.spacedBy(20.dp),
) {
    items(contentList) { contentType ->
        when (contentType) {
            is ContentType.Header -> HeaderContent()
            is ContentType.Red -> RedContent()
            is ContentType.Yellow -> YellowContent()
            is ContentType.Blue -> BlueContent()
            // 省略
        }
    }
}
```


## 遅延読み込みの実装
以下の手順で遅延読み込みを実装します。

#### 1. rememberLazyListState()を定義する
LazyColumnのスクロール状態を管理するために、 rememberLazyListState()を使用してLazyListStateオブジェクトを作成します。

#### 2. LazyColumnのstateに渡す
作成したLazyListStateオブジェクトを、LazyColumnのstateパラメータに渡します。これにより、LazyColumnのスクロール状態を監視できるようになります。

#### 3. `listState.layoutInfo.visibleItemsInfo`をsnapshotFlowで受け取る

`listState.layoutInfo.visibleItemsInfo`で現在画面に表示されているアイテムの情報を受け取ることができます。ただ、visibleItemsInfoは頻繁に更新される可能性があるため、不要な再コンポジションを発生させる可能性があります。そのため、snapshotFlowを使用して`listState.layoutInfo.visibleItemsInfo`を監視します。snapshotFlowを使用することで状態の変化をストリームとして受け取ることができます。
この監視とそれに伴う遅延読み込み処理は、Composable関数の外部で実行するためにLaunchedEffect内で実行します。

#### 4. 読み込み開始したいContentTypeで読み込み始める
visibleItemsInfoから読み込みを開始したいContentTypeが表示されているかどうかを判定します。今回はContentType.Greenに対応する要素が表示された時に読み込み処理を開始するようにしています。読み込み処理にかんしては、5秒後に次に読み込む要素を返す関数を呼び出しています。

#### 5. 無限読み込みを回避するためにisLoadingで制御する
読み込み処理が複数回実行されないように、isLoadingフラグを使用して制御します。読み込み処理が開始されたらisLoadingをtrueに設定し、読み込みが完了したらfalseに戻します。

ここまででコードは以下のようになります。
```kotlin
// 1. rememberLazyListState()を定義する
val listState = rememberLazyListState()
// 5. 無限読み込みを回避するためにisLoadingで制御する
var isLoading by remember { mutableStateOf(false) }

LaunchedEffect(key1 = Unit) {
    snapshotFlow { listState.layoutInfo.visibleItemsInfo }
        .collect { visibleItemsInfo ->
            // 4. 読み込み開始したいContentTypeで読み込み始める
            if (visibleItemsInfo.any { contentList[it.index] is ContentType.Green } && !isLoading) {
                isLoading = true
                contentList.add(ContentType.LoadingIndicator) // ロードインジケーターを追加
                // ここで、非同期で次のアイテムを読み込む処理を実行
                val newItems = loadMoreItems() 
                contentList.remove(ContentType.LoadingIndicator) // ロードインジケーターを削除
                contentList.addAll(newItems) // 新しいアイテムを追加
            }
        }
}

LazyColumn(
    modifier = Modifier
        .fillMaxWidth()
        .fillMaxHeight(),
    verticalArrangement = Arrangement.spacedBy(20.dp),
    state = listState // 2. LazyColumnのstateに渡す
) {
    // 省略
}

// 次のアイテムを読み込む関数
suspend fun loadMoreItems(): List<ContentType> {
    delay(5000)

    return listOf(
        ContentType.Magenta,
        ContentType.Cyan,
    )
}
```
## 結果
緑の要素が表示された時に読み込みが開始されて次の要素が表示されるようになりました🙌
![LazyLoad_sample.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2346644/b148c13f-768d-9771-1ff9-41b23f2804ed.gif)