Infinito Nirone 7

白羽の矢を刺すスタイル

BottomSheetScaffold の sheetContent を空にしてはいけない

BottomSheetScaffold を使って BottomSheet を作るとき、BottomSheet の中身は BottomSheetScaffold の引数にある sheetContent で作っていきます。

developer.android.com

BottomSheetScaffold(
  sheetContent = {
    /* BottomSheet の中身 */
  }
) { paddingValues ->
  /* 画面の中身 */
}

この画面において BottomSheet の表示に複数の種類がある場合を考えてみます。 種類に応じて sheetContent を切り替えたいので、次のような enum による分岐をしてみます。 今回は 2 種類の BottomSheet を一つの画面で表示する想定で BottomSheetType という enum を定義して sheetContent 内部で表示を切り替えます。 このとき、BottomSheet には開いた状態と閉じた状態の 2 種類があることを考慮に入れ、閉じた場合に sheetContent を表示しないようにするためなにもないことを示す値も合わせて定義してみます。

enum class BottomSheetType {
  NONE, FOO, BAR
}

var sheetType by remember { mutableStateOf(BottomSheetType.NONE) }
val bottomSheetState = rememberBottomSheetState(
  initialValue = BottomSheetValue.Collapsed,
)
val scaffoldState = rememberBottomSheetScaffoldState(
  bottomSheetState = bottomSheetState,
)

BottomSheetScaffold(
  scaffoldState = scaffoldState,
  sheetPeekHeight = 0.dp,
  sheetContent = {
    when (sheetType) {
      BottomSheetType.NONE -> {
        // 閉じた状態を表現したい。何も表示するものはないので空のまま。
      }
      BottomSheetType.FOO -> {
        Text(text = "foo")
      }
      BottomSheetType.BAR -> {
        Text(text = "bar")
      }
    }
  }
)

このようなコードで sheetContent を作る画面を動かすと BottomSheetState のもつ状態がおかしくなってしまいます。具体的には、rememberBottomSheetState で初期値を Collapsed にしていても、Composition がおわると Expanded な状態であると判定されてしまいます。

またこのような sheetContent の作り方を実装した上で BottomSheet を展開しようと次のようなコードを呼び出すとアプリがクラッシュします。

// Jetpack Compose 1.1.0 でクラッシュするコード (1.2.0 ではクラッシュしない)
coroutineScope.launch {
  bottomSheetState.expand()
}

// Jetpack Compose 1.2.0 でクラッシュするコード (1.1.0 ではクラッシュしない)
coroutineScope.launch {
  bottomSheetState.animateTo(BottomSheetValue.Collapsed)
}

クラッシュ時の例外は次の通りで、展開時のアニメーションを実行しようとしたとき、アニメーションが終了する位置が指定できていないことでクラッシュしていることがわかります。

java.lang.IllegalArgumentException: The target value must have an associated anchor.

そもそも BottomSheet を展開していないのに BottomSheetStateExpanded になってしまうことが良くないのですが、これは sheetContent で何も Composable を呼ばないケースが不具合を引き起こしています。 このため、次のように何も表示しないケースで Spacer(modifier = Modifier.height(1.dp)) を差し込むか、when 全体を Box で囲むか、そもそも enum の定義から表示しないパターンの定義を消すかのいずれかで、常に sheetContent が何かしらの Composable を呼び出すようにする必要があります。

// 小さな Spacer を差し込むパターン
BottomSheetScaffold(
  scaffoldState = scaffoldState,
  sheetPeekHeight = 0.dp,
  sheetContent = {
    when (sheetType) {
      BottomSheetType.NONE -> {
        Spacer(modifier = Modifier.height(1.dp))
      }
      BottomSheetType.FOO -> {
        Text(text = "foo")
      }
      BottomSheetType.BAR -> {
        Text(text = "bar")
      }
    }
  }
)

// when 全体を Box で囲むパターン。BottomSheetType.NONE でも最低 1dp は確保する
BottomSheetScaffold(
  scaffoldState = scaffoldState,
  sheetPeekHeight = 0.dp,
  sheetContent = {
    // 最低 1dp の高さは確保
    Box(modifier = Modifier.requiredHeightIn(min = 1.dp)) {
      when (sheetType) {
        BottomSheetType.NONE -> {
          // 閉じた状態を表現したい。何も表示するものはないので空のまま。
        }
        BottomSheetType.FOO -> {
          Text(text = "foo")
        }
        BottomSheetType.BAR -> {
          Text(text = "bar")
        }
      }
    }
  }
)

// enum の定義を削除するパターン
BottomSheetScaffold(
  scaffoldState = scaffoldState,
  sheetPeekHeight = 0.dp,
  sheetContent = {
    when (sheetType) {
      BottomSheetType.FOO -> {
        Text(text = "foo")
      }
      BottomSheetType.BAR -> {
        Text(text = "bar")
      }
    }
  }
)

参考リンク

stackoverflow.com