material3.ModalBottomSheetの使い方とTips・トラブルシューティング

Android Advent Calendar 2023の4日目の記事です。

昨年は、`androidx.compose.material` の `ModalBottomSheetLayout` に関する記事を投稿したのですが、今年は `androidx.compose.material3` でも `ModalBottomSheet` が提供されるようになりました。Material3になり、コンポーネントの使い方自体が `ModalBottomSheetLayout` とは異なる形に変更されています。

昨年の記事: [ModalBottomSheetLayoutの使い方とTips・トラブルシューティング](https://qiita.com/ntsk/items/29d22763dd0234bc9150)

本記事では、昨年の記事の続きとして `material3.ModalBottomSheet` の使い方やTips・トラブルシューティングについて紹介します。

※ 本記事の内容は、`androidx.compose.material3:material3-*:1.2.0-alpha12` 時点のものです。将来的に変更される可能性がありますのでご注意ください。

# material.ModalBottomSheetLayoutとの差分

Material2 から Materail3への移行については下記マイグレーションガイドがあります。

https://developer.android.com/jetpack/compose/designsystems/material2-material3

`ModalBottomSheetLayout` との差分として、コンポーネントの使い方やデザインのデフォルトが Material3 基準になっていることで変更点がいくつかあります。

## デザイン

大きく下記の点が変更されており、コンポーネントのデフォルトの挙動が下記に従ったものに変更されています。

- カラーマッピングの変更とダイナミックカラーへの対応
- 角丸(28dp) が適用され、Material2 と比較して柔らかい印象に
- Sheet を Expand するときに利用する DragHandle が追加され、iOSの [Sheets](https://developer.apple.com/design/human-interface-guidelines/sheets) に近いデザインに

https://m3.material.io/components/bottom-sheets/overview#0ccc1064-69de-49c2-8342-67dc8424306b

![スクリーンショット 0005-12-04 7.17.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33071/728e8ff2-9744-b15e-f0a1-81387707e891.png)


## 実装

`material.ModalBottomSheetLayout` では、レイアウトの Composable をwrapして実装し、show・hideを呼び出す必要があったのに対し、 `material3.ModalBottomSheet` ではコンポジションに入場した時だけ呼び出される形に変更されています。

### Before (material.ModalBottomSheetLayout)

```kotlin
val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)

ModalBottomSheetLayout(
    sheetState = sheetState,
    sheetContent = {
        // BottomSheet内に表示するコンテンツ
    },
    content = {
        // BottomSheet自体を表示したい画面のコンテンツ
    },
)
```

```kotlin
val coroutineScope = rememberCoroutineScope()

Button(
    onClick = {
        coroutineScope.launch {
            sheetState.show()
        }
    }
) {
    Text(text = "Button")
}
```

### After (material3.ModalBottomSheet)

```kotlin
val sheetState = rememberModalBottomSheetState()
val showModal = remember { mutableStateOf(false) }

if (showModal.value) {
    ModalBottomSheet(
        sheetState = sheetState,
        onDismissRequest = {
            showModal.value = false
        },
        component = {
            // BottomSheet内に表示するコンテンツ
        },
    )
}
```
```kotlin
Button(
    onClick = {
        showModal.value = true
    }
) {
    Text(text = "Button")
}
```

上記の例では、`showModal.value` が true の場合のみコンポジションに入場し、 `ModalBottomSheet` が描画されます。また、画面外をタップしたりスワイプによってシートを閉じた場合には `onDismissRequest` が呼び出されるため、このタイミングで `showModal.value` を false に戻すことで、シートを閉じることができます。

# Tips・トラブルシューティング

## ステータスバーの位置も暗転してほしい

昨年の記事では `material.ModalBottomSheetLayout` でステータスバー領域が暗転されないことの解決として、`WindowCompat.setDecorFitsSystemWindows(window, false)` + [AccompanistのSystemUiController](https://google.github.io/accompanist/systemuicontroller/) を利用してedgeToEdge対応することで回避する方法を紹介しました。

Accompanist SystemUiController は現在非推奨になっており、 `androidx.activity 1.8.0-alpha03` 以降では、[ComponentActivity.enableEdgeToEdge()](https://developer.android.com/reference/androidx/activity/ComponentActivity#(androidx.activity.ComponentActivity).enableEdgeToEdge(androidx.activity.SystemBarStyle,androidx.activity.SystemBarStyle)) を利用することが推奨されます。

### EdgeToEdge未対応の場合

`material3.ModalBottomSheet` ではこの問題が解決されており、edgeToEdge に対応していない場合、デフォルトでステータスバー領域まで暗転するようになっています。

<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33071/3ced0e3f-a5df-465a-7bcf-9c7daa435427.gif width=300>


### EdgeToEdge対応済の場合
edgeToEdge が有効な場合、デフォルトではステータスバー領域が暗転しません。

<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33071/5f1948f2-4e32-b2c2-4860-7880e1eae84b.gif width=300>

edgeToEdge が有効な状態でも暗転させるようにするには、1.2.0-alpha02で追加された `windowInsets` へ `0` もしくは `0.dp` を指定することで適用できます。

```kotlin
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()　
    }
}
```

```kotlin
ModalBottomSheet(
        sheetState = sheetState,
        onDismissRequest = {
            showModal.value = false
        },
        windowInsets = WindowInsets(0)
        component = {
            // BottomSheet内に表示するコンテンツ
        },
    )
```

コンテンツ領域にめり込んだ挙動になるのは変わりないので `systemBarPadding()` などを適用し調整しましょう。

<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33071/d6ce2aae-238f-3542-7488-454c7bdc110d.gif width=300>


## Material2相当のUIにしたい

`material.ModalBottomSheetLayout` との差分の章でも紹介しましたが、デザイン面の変更に伴ってデフォルトのデザインが変更されています。 Material3 ライブラリには移行したいがデザインは変えたくないというケースがあると思います。この場合、下記のようにすることで Material2 相当のデザインとなります。

```kotlin
ModalBottomSheet(
    shape = RoundedCornerShape(0.dp),
    sheetState = modalBottomSheetState,
    tonalElevation = 0.dp,
    dragHandle = {},
    onDismissRequest = {
        showModal.value = false
    },
) {
    // BottomSheet内に表示するコンテンツ
}
```

変更点は下記の3つです。

- 角丸の削除
    - `shape` を明示的に0dpを適用し角丸を無くします
- `tonalElevation` によるカラーの適用を行わない
    - Material3 では、 `Surface` へ `tonalElevation` の高さに応じてPrimaryカラーから計算された色が標高として適用されます。 `ModalBottomSheet` 内部では `Surface` が使われていることと、 `tonalElevation` はデフォルトでは 1dp が指定されていることで淡い色が自動的に適用されてしまいます。明示的に 0dp を指定することで適用されないようにすることができます
- dragHandleを削除
    - dragHandleは `BottomSheetDefaults.DragHandle()` がデフォルトでは適用されますが、カスタマイズできるようになっており、空を渡すことで削除できます

<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33071/4f5e10ab-7c34-b717-0071-b46e5c908a81.png width=300>

## IME表示時にTextFieldが上に飛んでいく

IME表示を行うとTextField位置がおかしくなる問題がありました。

https://issuetracker.google.com/issues/285746907

alpha10で修正されたため、alpha10以降を利用することをおすすめします。

| alpha10以前 | alpha10以降 |
|-|-|
|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33071/a4657034-5b2c-ee63-cb3d-a04e30c13153.gif" width="300" />|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33071/90ba39aa-54f1-ffb3-7660-30a37e6577ba.gif" width="300" />|


# 所感

2023年12月現在 material3-1.2.0 はまだ alpha12 ですが、 ModalBottomSheet 関連のバグが高頻度で修正されていますので、利用される場合には stable の 1.1.2 ではなく、最新を利用する方が良いでしょう。

昨年の[所感](https://qiita.com/ntsk/items/29d22763dd0234bc9150#%E6%89%80%E6%84%9F)で `material.ModalBottomSheetLayout` について複雑性が高く使いづらいと記載しましたが、 `material3.ModalBottomSheet` では、その問題がかなり解決されているように感じました。

レイアウトでwrapする必要がなくなったことで、複数の画面で同一のUIを利用したい場合にも Composable Function の共通化がしやすくなったと思います。