LoginSignup
4
2

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

Last updated at Posted at 2024-03-30

Dialogのざっくり紹介

Jetpack ComposeのDialogはAlertDialogを使うことで、簡単にダイアログの定義をすることができるため、XMLでの作成と比べてとても楽です。

@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による表示管理がよく使われています。

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

    if (!show) {
        return
    }
    
    AlertDialog(
        onDismissRequest = {
            show = false
        },
        // 省略
    )
}

あるいは

@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の表示切り替えをすることができるようになります。

実装例

@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の表示管理にも有効な方法ですので、お困りの際はぜひご活用ください。

4
2
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
4
2