モバイルアプリにおいてユーザーへの情報伝達は非常に重要です。その中でも、短時間でユーザーにフィードバックを伝える際に便利なUI要素としてSnackbarがあります。
この記事では、ComposeにおけるSnackbarの基本から、画面などのスコープを意識したSnackbarの状態・表示を CompositionLocal によりうまく扱う方法を紹介します。
Snackbarとは
Snackbarは、画面の下部に一時的に表示されるUI要素で、ユーザーへの短いメッセージやアクション可能なフィードバックを提供します。
Androidには類似のUI要素としてToastがあります。SnackbarはToastと以下のような点で異なります。表示期間・アクション・デザインといった機能性が注目されることが多いですが、この記事では「スコープ」に着目します。
Snackbarは表示する要因になった画面やアプリ内で表示されます。そのため、画面遷移などによって表示されなくなる必要があります。Snackbarを扱うとき、どの画面に表示するかは意識する必要があります。
| Snackbar | Toast | |
|---|---|---|
| 表示期間 | 自動消滅・手動消滅 | 自動消滅 |
| アクション | ボタンを配置可能 | 不可 |
| デザイン | Material Designに準拠、自由度が高い | 変更不可 |
| スコープ | 画面・アプリ(特定機能に対して) | デバイス全体(システムに対して) |
| 表示例 | ![]() |
![]() |
ComposeにおけるSnackbar
ComposeでSnackbarを表示するには、SnackbarHost と SnackbarHostState を使用します。
SnackbarHost はSnackbarの表示領域を定義するComposable関数です。通常、Scaffold(Material Designによる基本的な画面レイアウト)の snackbarHost に配置して利用します。これにより、Snackbarが画面の適切な位置に表示されるようになります。
Scaffold(
snackbarHost = {
SnackbarHost(
hostState = snackbarHostState,
)
},
// ...
)
そして、実際にSnackbarの表示・非表示やメッセージ内容などの制御を行うのが SnackbarHostState です。これはUIの状態を保持するステートホルダーであり、Composable関数から利用する場合は remember を使って状態を記憶させます。
val snackbarHostState = remember { SnackbarHostState() }
Snackbarを表示するには、SnackbarHostState の showSnackbar を呼び出します。このメソッドはsuspend関数であるため、コルーチン内で呼び出す必要があります。
val coroutineScope = rememberCoroutineScope()
Button(
onClick = {
coroutineScope.launch {
snackbarHostState.showSnackbar(message = "message")
}
},
) {
Text(text = "Show Snackbar")
}
CompositionLocal とは
Snackbarは画面などの特定のスコープを意識して表示を切り替える必要があります。そのため、画面ごとに SnackbarHost と SnackbarHostState が必要になるのが一般的です。しかし、アプリ内で深くネストされた子・孫コンポーネントからSnackbarを表示したい場合、SnackbarHostState を親コンポーネントから引数として順々に渡していくバケツリレー(Props Drilling)が発生し、コードの保守性が低下することがあります。
そこで、CompositionLocal が役立ちます。CompositionLocal は、Composeのcomposition を通じてデータやサービスを暗黙的に「渡す」ための仕組みです。通常の引数によるデータ渡しとは異なり、階層の深い部分にあるComposable関数でも、親から明示的に引数として渡されなくても、特定の CompositionLocal で提供された値にアクセスできるようになります。
これにより、「この画面ではこの SnackbarHostState を利用する」といった、スコープに紐づいたSnackbarの管理が可能になります。子コンポーネントは、自動的にそのスコープに合わせた SnackbarHostState を利用できるため、コードがよりすっきりと記述できます。
CompositionLocal を使ってSnackbarを管理する具体的な例を紹介します。まず、SnackbarHostState を保持する CompositionLocal を定義します。
val LocalSnackbarHostState = staticCompositionLocalOf { SnackbarHostState() }
Scaffold に対して
Scaffold を含むComposable関数で LocalSnackbarHostStateを提供し、その下位のComposable 関数から SnackbarHostState を利用できるようにします。
このように MyScaffold を実装しておけば、それ以下のComposable関数からでも LocalSnackbarHostState.current を使って SnackbarHostState を取得し、その画面にSnackbarを表示できます。つまり、この Something は、MyScaffold 内に配置されていれば、MyScaffold にSnackbarを表示します。
@Composable
fun MyScaffold(
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
// ...
) {
CompositionLocalProvider(
LocalSnackbarHostState provides snackbarHostState,
) {
Scaffold(
snackbarHost = {
SnackbarHost(hostState = snackbarHostState)
},
// ...
)
}
}
@Composable
fun Something() {
val snackbarHostState = LocalSnackbarHostState.current
val coroutineScope = rememberCoroutineScope()
Button(
onClick = {
coroutineScope.launch {
snackbarHostState.showSnackbar(message = "Message")
}
},
) {
Text(text = "Show Snackbar")
}
}
NavGraphBuilder.composable に対して
Scaffold を利用しない場合や、Scaffold よりも広い範囲でSnackbarのスコープを管理したい場合は、ナビゲーショングラフ定義時に SnackbarHostState を提供すると良いでしょう。この場合も同様に CompositionLocalProvider を利用できます。これにより、特定の画面(ルート)に遷移したときに、その画面でのみ有効な SnackbarHostState が CompositionLocal として提供されます。
このように NavGraphBuilder.myComposable を実装した場合、LocalSnackbarHostState.current を使って同様の SnackbarHostState を取得できます。
inline fun <reified T : Any> NavGraphBuilder.myComposable(
// ...
noinline content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit,
) = composable<T>(/* ... */) { entry ->
CompositionLocalProvider(
LocalSnackbarHostState provides remember { SnackbarHostState() },
) {
content(entry)
}
}
他にも便利なこと
CompositionLocal を使うと、コンポーネントから直接Snackbarを操作できるようになるだけでなく、よりアプリケーション全体で利用可能な共通ロジックを実装する際に便利です。例えば、以下のようにURIを開く処理でエラーが発生した場合にSnackbar を表示する独自の UriHandler を定義できます。
この rememberMyUriHandler は、LocalSnackbarHostState に依存しています。もし、LocalSnackbarHostState が CompositionLocal として提供されていなければ、これを呼び出すたびに SnackbarHostState を引数で渡す必要があり、使い勝手が悪いです。
@Composable
fun rememberMyUriHandler(): UriHandler {
val uriHandler = LocalUriHandler.current
val snackbarHostState = LocalSnackbarHostState.current
val coroutineScope = rememberCoroutineScope()
val onFailure: State<(uri: String) -> Unit> = rememberUpdatedState { uri ->
coroutineScope.launch {
snackbarHostState.showSnackbar(message = "Can't open $uri.")
}
}
return remember(uriHandler, onFailure) {
object : UriHandler {
override fun openUri(uri: String) {
try {
uriHandler.openUri(uri)
} catch (e: IllegalArgumentException) {
onFailure.value(uri)
}
}
}
}
}
まとめ
この記事では、ComposeにおけるSnackbarの基本から、SnackbarHostState を CompositionLocal を使って管理する方法について紹介しました。CompositionLocal を使うことで、Snackbarのスコープをうまく扱うことができ、以下のようなメリットがあります。
- スコープ範囲の明確化: 子コンポーネントが、そのスコープに紐づくSnackbarを表示できるようになる
- コードの集約:
SnackbarHostStateを扱うためのコードが1箇所に集約され、他のコンポーネントは表示のための実装に専念しやすくなる - バケツリレーの解消:
SnackbarHostStateを引数で渡す必要がなくなるため、コードの可読性と保守性が向上する
参考文献

