🚀

副作用を使ったComposeのDialogの表示制御

2023/07/06に公開

androidx.appcompat.app.AlertDialogから、
androidx.compose.material.AlertDialog
androidx.compose.material3.AlertDialogに置き換える時、新しい概念である

表示状態

について考慮する必要があります。
androidx.appcompat.app.AlertDialogでは

AlertDialog.Builder(context)
	.create()
	.show()

とすることで表示することができ、ボタンを押したりダイアログの外をタッチすることで消すことができました。
しかし、Composable関数であるandroidx.compose.material(3).AlertDialogでは、ボタンを押したりダイアログの外をタッチすることでダイアログを自動で消すということができません。
Booleanの変数を用いて、ダイアログが表示されているか/されていないかを自分で制御して実装しなければなりません。
これはComposable関数の、冪等性に由来します。

この関数は、同じ引数で複数回呼び出されても同じように動作します。グローバル変数などの他の値、または random() への呼び出しは使用しません。

同じ引数で複数回呼び出されても同じように動作するということは、表示されている状態では常に表示されていないといけないし、表示されない状態では常に非表示でないといけません。また、グローバル変数などの他の値を使ってはいけないということはComposable関数の外でダイアログの表示状態を制御できないということです。
よくあるcomposeのdialogの実装記事では、同じComposable関数のなかでButtonを実装し、そのButtonでdialogの表示状態を制御するというものがあります。
この場合、remember関数を用いて状態を保持しています。
しかしこのルールに従うと、dialogの表示トリガーを全てComposableのコンポーネントに委ねる必要があり、自由度が高くありません。例えば、ネットワークリクエストに失敗したらdialogを表示したい、というような場合はViewによらない表示トリガーを、Composable関数の外に置く必要があります。
このような冪等性から外れる例外挙動を、副作用と言います。
この副作用を使って、Composable外からダイアログを制御してみたいと思います。

さて、状態・状態と言いながら、ダイアログの表示自体はどちらかというとone shot eventであると思うので、
EventとしてComposable関数に組み込むことを考えます。
(uiState, uiEventの違いについてはこちらを参考にしました。)

MyViewModel.kt
sealed interface UiEvent {
    object Error : UiEvent
}

private val _uiEvent: Channel<UiEvent> = Channel()
val uiEvent: Flow<UiEvent> = _uiEvent.receiveAsFlow()

fun somethingHappened() {
    _uiEvent.send(UiEvent.Error)
}

ViewModelでイベントを定義します。

CustomDialog.kt
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CustomDialog(viewModel: MyViewModel = viewModel()) {
    var showDialog by remember { mutableStateOf(false) }
    val onDismiss = { showDialog = false }
    LaunchedEffect(key1 = Unit) {
	viewModel.uiEvent.collect { uiEvent ->
	    when (uiEvent) {
		is UiEvent.Error -> {
		    showDialog = true
		}
	    }
	}
    }
    if (showDialog) {
	AlertDialog(
	    onDismissRequest = onDismiss
	    // TODO
	)
    }
}

LaunchedEffectの副作用を使って、Composable外のイベントをcollectしそれをトリガーにAlertDialogを表示させることができます。

kotlinCompilerExtensionVersion = "1.4.3"
"androidx.compose.material:material:1.3.1"

株式会社THIRD エンジニアブログ

Discussion