Tech Sheets

Jetpack Composeで引っ張って更新を実装する

最終更新日:2024-09-29

Material3のバージョン1.3.0で使い方が変わりました!

Jetpack Composeで引っ張って更新(PullToRefresh)を実装するには、以下の3ステップをやります。

  • UiStateに更新中の状態を追加する
  • LazyColumnを PullToRefreshBox で囲む
  • refresh中の時の処理を書く

更新中の状態を追加する

リスト表示を実装する際、UiStateで表示内容となるリストを管理していると思います。これに更新中かどうかを表す状態を追加します。

class MyViewModel : ViewModel() {
  sealed interface UiState {
    data object Initial : UiState
    data object Loading : UiState
    data class Success(
      val data: List<String>,
      val isRefreshing: Boolean, // 追加する
    ) : UiState

    data class Error(val message: String) : UiState
  }

  private val _uiState = MutableStateFlow<UiState>(UiState.Initial)
  val uiState = _uiState.asStateFlow()
  ...
}

LazyColumnを PullToRefreshBox で囲む

1.3.0から、LazyColumnを PullToRefreshBox で囲むだけでよくなりました。

PullToRefreshBox(
  isRefreshing = uiState.isRefreshing,
  onRefresh = {
    refresh() // 更新処理
  },
) {
  LazyColumn(
    modifier = Modifier
      .fillMaxWidth()
      .padding(innerPadding),
  ) {
    items(uiState.data) { item -> ... }
  }
}

refresh中の時の処理を書く

isRefreshing を一旦 true にしてから更新処理をViewModelに書いていきます。

fun refresh() {
  val currentState = _uiState.value
  if (currentState !is UiState.Success) return

  _uiState.value = currentState.copy(isRefreshing = true)
  viewModelScope.launch {
    try {
      val data = myRepository.fetch()
      _uiState.value = UiState.Success(
        data = data,
        isRefreshing = false,
      )
    } catch (e: CancellationException) {
      throw e
    } catch (e: Exception) {
      _uiState.value = UiState.Error(e.message ?: "Unknown error")
    }
  }
}

以下、どうしても1.3.0未満を使わないといけない方向け

Jetpack Composeで引っ張って更新(PullToRefresh)を実装するには、以下の4ステップをやります。

  • stateを作る
  • nestedScrollを設定する
  • PullToRefreshContainerを配置する
  • refresh中の時の処理を書く

stateを作る

rememberPullToRefreshState() を使います。

val pullToRefreshState = rememberPullToRefreshState()

nestedScrollを設定する

Scaffoldに対しnestedScrollを設定します。

Scaffold(
  modifier = Modifier.nestedScroll(pullToRefreshState.nestedScrollConnection),
) { innerPadding -> }

PullToRefreshContainerを配置する

読込中を表すコンポーザブルを配置します。Box + LazyColumnといった構成にします。

Box(modifier = Modifier.padding(innerPadding)) {
  LazyColumn(modifier = Modifier.fillMaxSize()) { ... }

  PullToRefreshContainer(
    state = pullToRefreshState,
    modifier = Modifier.align(Alignment.TopCenter),
  )
}

refresh中の時の処理を書く

isRefreshing を見て、 LaunchedEffect を使ってリフレッシュ処理を実行します。

if (pullToRefreshState.isRefreshing) {
  LaunchedEffect(Unit) {
    viewModel.refresh()
  }
}

完成形

@Composable
fun MainScreen(viewModel: MainViewModel) {
  val pullToRefreshState = rememberPullToRefreshState()

  Scaffold(
    modifier = Modifier.nestedScroll(pullToRefreshState.nestedScrollConnection),
  ) { innerPadding ->
    Box(modifier = Modifier.padding(innerPadding)) {
      LazyColumn(modifier = Modifier.fillMaxSize()) { ... }

      PullToRefreshContainer(
        state = pullToRefreshState,
        modifier = Modifier.align(Alignment.TopCenter),
      )
    }
  }

  if (pullToRefreshState.isRefreshing) {
    LaunchedEffect(Unit) {
      viewModel.refresh()
    }
  }
}

一覧に戻る