0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

LocalSnackbarHostState: CompositionLocal による SnackbarHostState の管理方法

Posted at

モバイルアプリにおいてユーザーへの情報伝達は非常に重要です。その中でも、短時間でユーザーにフィードバックを伝える際に便利な UI 要素として Snackbar があります。

この記事では、Compose における Snackbar の基本から、画面などのスコープを意識した Snackbar の状態・表示を CompositionLocal によりうまく扱う方法を紹介します。

Snackbar とは

Snackbar は、画面の下部に一時的に表示される UI 要素で、ユーザーへの短いメッセージやアクション可能なフィードバックを提供します。

Android には類似の UI 要素として Toast があります。Snackbar は Toast と以下のような点で異なります。表示期間・アクション・デザインといった機能性が注目されることが多いですが、この記事では「スコープ」に着目します。

Snackbar は表示する要因になった画面やアプリ内で表示されます。そのため、画面遷移などによって表示されなくなる必要があります。Snackbar を扱うとき、どの画面に表示するかは意識する必要があります。

Snackbar Toast
表示期間 自動消滅・手動消滅 自動消滅
アクション ボタンを配置可能 不可
デザイン Material Design に準拠、自由度が高い 変更不可
スコープ 画面・アプリ(特定機能に対して) デバイス全体(システムに対して)
表示例 snackbar.gif toast.gif

Compose における Snackbar

Compose で Snackbar を表示するには、SnackbarHostSnackbarHostState を使用します。

SnackbarHost は Snackbar の表示領域を定義する Composable 関数です。通常、Scaffold(Material Design による基本的な画面レイアウト)の snackbarHost に配置して利用します。これにより、Snackbar が画面の適切な位置に表示されるようになります。

Scaffold(
    snackbarHost = {
        SnackbarHost(
            hostState = snackbarHostState,
        )
    },
    // ...
)

そして、実際に Snackbar の表示・非表示やメッセージ内容などの制御を行うのが SnackbarHostState です。これは UI の状態を保持するステートホルダーであり、Composable 関数から利用する場合は remember を使って状態を記憶させます。

val snackbarHostState = remember { SnackbarHostState() }

Snackbar を表示するには、SnackbarHostStateshowSnackbar を呼び出します。このメソッドは suspend 関数であるため、コルーチン内で呼び出す必要があります。

val coroutineScope = rememberCoroutineScope()
Button(
    onClick = {
        coroutineScope.launch {
            snackbarHostState.showSnackbar(message = "message")
        }
    },
) {
    Text(text = "Show Snackbar")
}

CompositionLocal とは

Snackbar は画面などの特定のスコープを意識して表示を切り替える必要があります。そのため、画面ごとに SnackbarHostSnackbarHostState が必要になるのが一般的です。しかし、アプリ内で深くネストされた子・孫コンポーネントから 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 を利用できます。これにより、特定の画面(ルート)に遷移したときに、その画面でのみ有効な SnackbarHostStateCompositionLocal として提供されます。

このように 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 に依存しています。もし、LocalSnackbarHostStateCompositionLocal として提供されていなければ、これを呼び出すたびに 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 の基本から、SnackbarHostStateCompositionLocal を使って管理する方法について紹介しました。CompositionLocal を使うことで、Snackbar のスコープをうまく扱うことができ、以下のようなメリットがあります。

  • スコープ範囲の明確化: 子コンポーネントが、そのスコープに紐づく Snackbar を表示できるようになる
  • コードの集約: SnackbarHostState を扱うためのコードが 1 箇所に集約され、他のコンポーネントは表示のための実装に専念しやすくなる
  • バケツリレーの解消: SnackbarHostState を引数で渡す必要がなくなるため、コードの可読性と保守性が向上する

参考文献

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?