【Android】PullToRefreshBoxを使って引っ張って更新機能を実装する

## はじめに
アプリでよく見る引っ張って更新する機能ですが、Material3のPullToRefreshBox使って引っ張って更新機能を作ってみたいと思います。

PullToRefreshBoxの詳しい内容は[こちら](https://developer.android.com/reference/kotlin/androidx/compose/material3/pulltorefresh/package-summary#PullToRefreshBox(kotlin.Boolean,kotlin.Function0,androidx.compose.ui.Modifier,androidx.compose.material3.pulltorefresh.PullToRefreshState,androidx.compose.ui.Alignment,kotlin.Function1,kotlin.Function1))を参照ください。

## PullToRefreshBoxを使って引っ張って更新機能を実装
### PullToRefreshBoxでUIを作成
以下は単純なリストを表示するComposableです。
リストを下に引っ張ると更新機能が走るように、PullToRefreshBoxを使っています。

``` sample.kt
class MainActivity : ComponentActivity() {
    private val viewModel: ListScreenViewModel by viewModels()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            PullToRefreshBoxSampleTheme {
                val uiState by viewModel.uiState.collectAsState()
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                        ListScreen(uiState, viewModel::refresh, innerPadding)
                }
            }
        }
    }
}


@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ListScreen(
    uiState: ListScreenViewModel.UiState,
    refreshAction: () -> Unit,
    padding: PaddingValues
) {
    val list = (uiState as? ListScreenViewModel.UiState.Success)?.data ?: emptyList()
    val isRefreshing = (uiState as? ListScreenViewModel.UiState.Success)?.isRefreshing ?: false

    PullToRefreshBox(
        isRefreshing = isRefreshing,
        onRefresh = {
            refreshAction()
        }
    ) {
        LazyColumn(
            modifier = Modifier
                .fillMaxSize()
                .padding(padding)
        ) {
            items(list.size) {
                Text("Item $it")
            }
        }
    }
}
```

PullToRefreshBoxの引数で設定している2つについて解説します。
- isRefreshing：リフレッシュが行われているかどうかを設定します。trueが設定されている場合、リストの上にローディングインジケーターが表示されます。
- onRefresh：引っ張るアクションを実行した時に動く処理を設定します。

### ViewModelを用意
UiStateを用意し、表示するデータとリフレッシュの実行中かどうかを設定するisRefreshingを持たせています。

``` sample.kt
class ListScreenViewModel : ViewModel() {
    sealed interface UiState {
        data class Success(val data: List<String>, val isRefreshing: Boolean) : UiState
    }

    private val _uiState = MutableStateFlow<UiState>(UiState.Success(listOf("1", "2", "3"), false))
    val uiState: StateFlow<UiState> = _uiState.asStateFlow()

    // 引っ張って更新時に動かす処理
    fun refresh() {
        val currentUiState = _uiState.value
        if (currentUiState is UiState.Success) {
            _uiState.value = currentUiState.copy(isRefreshing = true)
        }

        viewModelScope.launch {
            delay(2000)
            val currentData = (currentUiState as UiState.Success).data
            val newData = currentData + (currentData.size + 1).toString()
            _uiState.value = UiState.Success(newData, false)
        }
    }
}
```

refresh関数の中身について解説します。
画面で引っ張って更新が実行されたら、表示データは保持しつつ、isRefreshingのフラグをtrueに設定します。
こうすることで、画面にはローディングインジケーターが表示されるようになります。

isRefreshingをtrueに設定した後、coroutineを立ち上げてリストの更新処理を実行しています。
今回はサンプル処理として、2秒遅延後にリストのデータを一つ増やすような更新処理をしています。
更新処理の最後に、uiStateに更新後のデータとisRefreshingのフラグをtrueに設定してリフレッシュ処理を終了します。


### 実際に動かしてみた時の動き
前述のコードで実際に動かしてみた時の動きになります。
画面を下に引っ張るとローディングが表示され、リストが更新・ローディング非表示となっています。
![サンプル.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/512668/01876668-4ae0-beac-fb9d-7fbf7d8dd817.gif)