UiEventを使ったCompose Dialogの表示/非表示

# Dialogのざっくり紹介
Jetpack ComposeのDialogは[AlertDialog](https://developer.android.com/develop/ui/compose/components/dialog?hl=ja)を使うことで、簡単にダイアログの定義をすることができるため、XMLでの作成と比べてとても楽です。
```Kotlin
@Composable
fun SampleDialog() {
    AlertDialog(
        onDismissRequest = {
            // Dialogの枠外タップ時の挙動
        },
        confirmButton = {
            // OKとか確認とかのボタンの実装
            // 空だとボタン表示自体がされなくなる
        },
        dismissButton = {
            // キャンセル系のボタンの実装
            // 空でもOK
        },
        title = {
            // Dialogのタイトル
        },
        text = {
            //　Dialogの本文
            // といいつつここにButtonやらを仕込んでボタン並んでるようなDialogも作れる
            // textというよりもbody的な意味合いの方が強い
        }
    )
}
```
こんな簡単な実装で使いまわせるDialogを実装することができます。

# ちょっと面倒なところ
Dialog自体の表示処理やdismiss時の非表示処理は自分で記載する必要があります。
ここはFragment, XMLでのDialogとは異なる部分です。

簡単にやるならDialogのクラス内で`remember`を使用したStateによる表示管理がよく使われています。

```Kotlin
@Composable
fun SampleDialog() {
    // Stateによる状態監視を追加
    var show by remember { mutableStateOf(true) }

    if (!show) {
        return
    }
    
    AlertDialog(
        onDismissRequest = {
            show = false
        },
        // 省略
    )
}
```
あるいは

```Kotlin
@Composable
fun SampleScreen() {
    Scaffold(
        // 省略
    ) {
        var show by remember { mutableStateOf(true) }
        if (show) {
            SampleDialog(
                // callbackでボタンタップを受け取ってState切り替え
                onDismiss = {
                    show = false
                }
            )
        }
    }
}
```
などですね。

# Compose上のStateによる表示管理の問題
Compose側でDialogの表示制御をすることになるので、たとえば通信などの非同期処理な何かしらの処理をViewModel以下で実施後にその結果によってDialogを表示させたい時に対応できません。

# 解決策
UiEventを用いることで、自好きなタイミングでDialogの表示切り替えをすることができるようになります。

## 実装例
```Kotlin
@Composable
fun SampleScreen(
    state: SampleScreenState
) {
    Scaffold(
        // 省略
    ) {
        // uiEventの状態監視
        val uiEvents by state.uiEvents.collectAsState()
        uiEvents.forEach { event ->
            when (event) {
                SampleScreenViewModel.UiEvent.ShowDialog -> {
                    SampleDialog(
                        onDismiss = {
                            state.consume(target = event)
                        }
                    )
                }
            }
        }
    }
}

@Composable
private fun rememberSampleScreenState(
    viewModel: SampleScreenViewModel = viewModel {
        SampleScreenViewModel()
    }
): SampleScreenState = remember {
    SampleScreenState(
        viewModel = viewModel
    )
}

@State
class SampleScreenState(
    private val viewModel: SampleScreenViewModel
) {
    val uiEvents = viewModel.uiEvents

    fun showDialog() = viewModel.showDialog()

    fun consume(target: SampleScreenViewModel.UiEvent) =
        viewModel.consume(target = target)
}

class SampleScreenViewModel() : ViewModel() {
    sealed interface UiEvent {
        object ShowDialog : UiEvent
        // 引数をつけたい場合は
        // data class ShowDialog(val arg: String) : UiEvent
    }

    private val _uiEvents: MutableStateFlow<List<UiEvent>> = 
        MutableStateFlow(emptyList())
    val uiEvents = _uiEvents.asStateFlow()

    // UiEventにShowDialogを追加する
    fun showDialog() {
        _uiEvents.update {
            it + UiEvent.ShowDialog
        }
    }

    // UiEventからtargetで指定したUiEventを削除
    fun consume(target: UiEvent) {
        _uiEvents.update { e -> e.filterNot { it == target } }
    }
}
```

これによって、画面上のボタンタップ時にDialogを表示したり、非同期処理の完了後にDialogを表示したりなど、かなり広い範囲の表示パターンでの表示管理をカバーすることができます。

Snackbarなどのその他UIの表示管理にも有効な方法ですので、お困りの際はぜひご活用ください。