[Jetpack Compose] Paging Libraryを使わずに無限リストを実装する

末尾までスクロールすると追加の要素を読み込んでいくやつです。
Androidではこの無限スクロールを実装するのにPaging3を使うことで簡単に（？）実装することができます。

https://developer.android.com/jetpack/androidx/releases/paging

Jetpack Compose対応もされているので、そちらを使って無限スクロール実装するのがベターな気がしますが、それを使わなくともかなりシンプルに実装することができましたので紹介します🙋‍♂️

## 完成図

25件ずつ追加で読み込んでいます。サンプルコードなので通信処理はなしでListのジョインをしているのでローディング表示なくスルスルいきます。
末尾にはローディングがあるので読み込みに時間がかかる場合は見える感じです。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166629/94b775b0-66db-97a8-c8ad-7e308330fb8f.gif" width=320 />

## 実装の要約（TL;DR）

- LazyColumnの末尾に `CircularProgressIndicator` を配置
- `CircularProgressIndicator` の `LaunchedEffect` にて要素の追加取得処理

## 要素の末尾にインジケーターを追加する

インジケーターじゃなくてもいいんですが、今回はインジケーターを追加しています。

実際は色々Modifier定義があったりして見づらいので簡略コードですが、こんな感じです。なにも難しいことはしてないです。要素の末尾にローディングを追加しているだけ。

```kotlin
LazyColumn {
    listItem.forEach { item ->
        item { Card(item) }    
    }
    item { CircularProgressIndicator() }
}
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166629/3c90dcb1-6174-67e6-8e5c-b2f39ec93c8a.gif" width=320 />

こんな感じで要素の最後まで行ったらローディングが表示される感じです。

## 末尾の要素のComposableで追加読み込みを発火する

`LaunchedEffect` を使うとComposabelが入場したタイミングで発火するCoroutine関数を実行することができます。
これを使うことで末尾の要素が画面に追加されたタイミングで要素の追加読み込みを行うことができました。

こちらも簡易コードですが、こんな感じです。

```kotlin
@Composable
fun list() {
    val items = viewModel.listItems.observeAsState()
    LazyColumn {
        items.value.forEach { item ->
            item { Card(item) }    
        }
        item { LoadingIndicator() }
    }
}

@Composable
fun LoadingIndicator() {
    CircularProgressIndicator(modifier = modifier)

   LaunchedEffect(key1 = true) {
       // 要素の追加読み込み
       viewModel.fetchItems()
   }
}
```

要素の追加読み込みの処理自体はViewModelで行うと思います。
そしてViewModelで保持しているLiveDataかFlowの値をLazyColumnで表示していると思うので、自動で再Composeが走りリスト表示の要素が追加されてます。

またスクロールしていき追加した分の末尾に到着したら同様にローディングが表示され要素が追加されていきます。

追加読み込み分もない本当の終わりに到着した場合はif文などでローディング表示のCompossableを表示しなければ `LaunchedEffect` は発火しません。かんたん。

```kotlin
@Composable
fun list() {
    val items = viewModel.listItems.observeAsState()
    val isLast = viewMode.isLast.observeAsState()
    LazyColumn {
        items.value.forEach { item ->
            item { Card(item) }    
        }

        if (isLast.value.not()) {
            item { LoadingIndicator() }
        }
    }
}
```

実際に実装した例です。
25件ずつ追加読み込みを行い、100件まで到達したら止まります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166629/852efbbc-39cf-47f5-4483-3698ac13a237.gif" width=320 />

## サンプルコード

最後に今回のGifで使ったサンプルコードの全コードです。
サンプルではViewModelは使わずにremeberを使いそのまま要素の追加を実装しています。

<details><summary>サンプルコード</summary><div>

```kotlin
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.github.yasukotelin.jetpackcomposeplayground.ui.theme.JetpackComposePlaygroundTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            JetpackComposePlaygroundTheme {
                MainScreen()
            }
        }
    }
}

const val MaxCount = 100
const val AddCount = 25

@Composable
fun MainScreen() {
    val listItem = remember { mutableStateOf(createListItem(end = 0, count = AddCount)) }

    Surface(color = MaterialTheme.colors.background) {
        LazyColumn(
            modifier = Modifier.fillMaxSize(),
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            item { Spacer(modifier = Modifier.height(16.dp)) }
            item { Header() }
            item { Spacer(modifier = Modifier.height(16.dp)) }
            listItem.value.forEach {
                item {
                    ItemCard(
                        name = it,
                        modifier = Modifier.padding(bottom = 8.dp, start = 32.dp, end = 32.dp)
                    )
                }
            }

            val isLast = listItem.value.count() >= MaxCount
            if (isLast.not()) {
                item {
                    LoadingIndicator(
                        key = listItem.value.first(),
                        modifier = Modifier.padding(top = 16.dp, bottom = 32.dp),
                        onLaunch = {
                            val endCount = listItem.value.count()
                            listItem.value = listItem.value + createListItem(endCount, AddCount)
                        }
                    )
                }
            }
        }
    }
}

@Composable
fun Header() {
    Box(
        contentAlignment = Alignment.Center,
        modifier = Modifier
            .height(150.dp)
            .fillMaxWidth(),
    ) {
        Text("Infinite scroll", fontSize = 22.sp)
    }
}

@Composable
fun ItemCard(modifier: Modifier = Modifier, name: String) {
    Card(
        modifier = modifier
            .fillMaxWidth()
            .height(80.dp)
    ) {
        Box(
            modifier = Modifier.fillMaxSize(),
            contentAlignment = Alignment.Center
        ) {
            Text(text = name)
        }
    }
}

@Composable
fun LoadingIndicator(key: String, modifier: Modifier = Modifier, onLaunch: () -> Unit) {
    CircularProgressIndicator(modifier = modifier)

    LaunchedEffect(key1 = true) {
        Log.d("LaunchedEffect", "Launch! key is $key")
        onLaunch()
    }
}

private fun createListItem(end: Int, count: Int): List<String> =
    (end + 1..end + count).toList().map { "List item $it" }

@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    JetpackComposePlaygroundTheme {

    }
}
```

</div></details>

## まとめ

RecyclerViewでは無限リストを実装するのに手間がかかったりして、Paging Libraryのようなものが登場したりして、それでも正直面倒だし大変でしたが（そもそもRecyclerViewが難しい）、Composeになると簡単な気がします。
パフォーマンス的な部分などでComposeでもPaging Libraryを使ったほうがいいのかもしれませんが、簡易にも実装できそうですね。