ScaffoldでないComposeでSnackBarを出す

掲題の通りです。
Compose化の初期段階で、まだFragmentは残しつつ、中身だけフルComposeにしているという状況で、SnackBarをどう出したら良いかと一晩悩みました。

レイアウトxmlファイルを残して、そこにComposeViewを入れて出す方法も最悪有りかなと考えましたが、結論、以下のコードを元にいじって作成できました。

https://medium.com/@213maheta/snackbar-without-scaffold-in-jetpack-compose-c6868f908100

import文は長いのですが候補を選ぶの間違えるとビルド通らずに困ると思うので以下を展開して見てください。

<details><summary>import</summary>

```kotlin
import androidx.annotation.StringRes
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.launch
```
</details>

では、コードです。

```kotlin
@Composable
fun CustomSnackbar(
    message: String,
    textColor: Color,
    backgroundColor: Color,
    duration: SnackbarDuration = SnackbarDuration.Long,
    actionLabel: String = "",
    actionLabelColor: Color,
    onDismissed: () -> Unit,
    onActionClick: () -> Unit = {},
    verticalAlignment: Alignment = Alignment.BottomCenter,
) {
    val snackbarHostState = remember { SnackbarHostState() }
    val scope = rememberCoroutineScope()

    LaunchedEffect(key1 = message) {
        if (message.isNotEmpty()) {
            scope.launch {
                val result = snackbarHostState.showSnackbar(
                    message = message,
                    duration = duration,
                )
                when (result) {
                    SnackbarResult.Dismissed -> {
                        onDismissed.invoke()
                    }
                    else -> {}
                }
            }
        }
    }

    SnackbarHost(hostState = snackbarHostState) {
        SnackbarBody(
            message = message,
            textColor = textColor,
            backgroundColor = backgroundColor,
            actionLabel = actionLabel,
            actionLabelColor = actionLabelColor,
            onActionClick = onActionClick,
            verticalAlignment = verticalAlignment,
        )
    }
}
```

使っている側のSnackBar表示フラグなんかを戻す必要がある場合に備えて、dismissコールバックを設定できるようにしています。
Actionボタンのコールバックは本体の方で呼ぶのでそのまま渡しています。

`key1`に指定している`message`が変わるたびに、再表示されたときに`LaunchedEffect`が実施されます。つまり登場アニメーションが掛かります。
面倒なのでアニメーションはデフォルトのままです。

`SnackbarBody`が本体になります。

```kotlin
@Composable
private fun SnackbarBody(
    message: String,
    textColor: Color,
    backgroundColor: Color,
    actionLabel: String,
    actionLabelColor: Color,
    onActionClick: () -> Unit = {},
    verticalAlignment: Alignment = Alignment.BottomCenter,
) {
    Box(
        modifier = Modifier
            .fillMaxSize(),
        contentAlignment = verticalAlignment,
    ) {
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .height(68.dp)
                .padding(10.dp)
                .graphicsLayer {
                    shadowElevation = 15f
                }
                .background(
                    color = backgroundColor,
                    shape = RoundedCornerShape(4.dp),
                ),
            verticalAlignment = Alignment.CenterVertically,
            horizontalArrangement = Arrangement.Center,
        ) {
            Text(
                modifier = Modifier
                    .weight(1f)
                    .padding(horizontal = 10.dp),
                text = message,
                color = textColor,
                fontSize = 14.sp,
            )
            if (actionLabel.isNotEmpty()) {
                TextButton(
                    onClick = onActionClick,
                ) {
                    Text(
                        text = actionLabel,
                        modifier = Modifier,
                        color = actionLabelColor,
                        fontSize = 14.sp,
                    )
                }
            }
        }
    }
}
```

プレビューコードはこんな感じで。

```kotlin
@Preview(showBackground = true, widthDp = 320, heightDp = 480)
@Composable
fun CustomSnackbarPreview() {
    MyMaterialTheme {
        SnackbarBody(
            message = "エラーメッセージ本文サンプル",
            textColor = Color.White,
            backgroundColor = colorResource(id = R.color.bg_error),
            actionLabel = "閉じる",
            actionLabelColor = Color.White,
        )
    }
}
```

これでだいたい、Viewで`Snackbar.make(〜).show()`したのと同じ見た目で出せました。

どうでもいいけどCompose関連はまだまだ不安定ですね。
Android Studioがしょっちゅう重くなるし、常時「Up-to-date」が出たままになったり、フルリビルドしただけで動作が変わっちゃったりします。
いまもプレビュー結果のキャプチャをとろうとしたらプレビュー欄がLoadingのまま変わらなくなって1時間たつので、諦めました:sweat:

styleなんかもコードベースに移植しなきゃならないのが地味にきついです。
個々に設定しないでちゃんとstyleをまとめているプロジェクトほど移行がきついという・・・