- 悪い例:境界を越えたか分からないイベント
- 何が問題なのか
- 良い例:境界を越えたことが分かるイベント
- ここで何が起きているか
- イベントが「境界を越えた通知」になる理由
- なぜ sealed class が効くのか
- この章のまとめ
第8章では、 本書における「イベント」を次のように定義しました。
イベントとは、「判断が完了したこと」を表す構造 です。
これは、
- onClick
- onTextChanged
- onResume
といった UI やフレームワークが発火する通知 とは、 まったく別の概念です。
ここでは、 この違いがコードとしてどう現れるのかを、 具体例で確認します。
悪い例:境界を越えたか分からないイベント
まずは、よくある実装から見てみます。
class FormViewModel : ViewModel() {
private var uiState = FormUiState()
fun onTextChanged(text: String) {
uiState = uiState.copy(inputText = text)
}
fun onSubmitClicked() {
// 本当に送信してよい状態か?
// ここで毎回判断する必要がある
}
}
一見、問題なさそうに見えます。
しかし、この onSubmit() には、
次の疑問が常につきまといます。
- 入力は本当に完了しているのか
- バリデーションは終わっているのか
- この時点で「判断」は終わっているのか
コード上では、 どこで判断が完了したのかが分かりません。
つまりこの onSubmit() は、
- 境界を越えてよいのか分からない
- 毎回、中身を読まないと安全性が判断できない
という状態です。
これは、 イベントが「判断済み」であることを表現できていない 典型例です。
何が問題なのか
問題は、 「onSubmit という関数があること」ではありません。
問題は、
この呼び出しが、 判断の途中なのか、判断が完了した後なのかが、 型から分からない
ことです。
そのため、
- ViewModel 内で再度 if が必要になり
- レビューやテストに判断が押し出され
- 「正しく使ってね」という前提が増えていきます
これは、 第3章で述べた 判断が構造で表現されていない状態 です。
良い例:境界を越えたことが分かるイベント
ポイントは 1 つだけです。
ViewModel の中に「未判断の世界」と「判断済みの世界」を両方置き、 その境界を Event で表現する
状態(未判断の世界)
data class FormUiState(
val inputText: String = ""
)
これは、
- 入力途中
- 編集中
- まだ Submit してよいか分からない
未判断の世界の状態 です。
イベント(判断完了の世界)
sealed interface FormEvent {
data class Submit(
val text: String
) : FormEvent
}
この Event は、
- バリデーションが終わり
- Submit してよいと判断された
判断完了の結果 だけを表します。
ViewModel(判断を完了させる場所)
class FormViewModel : ViewModel() {
private var uiState = FormUiState()
fun onTextChanged(text: String) {
uiState = uiState.copy(inputText = text)
}
fun onSubmitClicked() {
val text = uiState.inputText
// ここで判断を行う
if (text.isBlank()) {
// 判断を通過できなかったデータは境界を越えられない
return
}
val event = FormEvent.Submit(text)
handleEvent(event)
}
private fun handleEvent(event: FormEvent) {
when (event) {
is FormEvent.Submit -> {
// ここでは「判断済み」であることを信じてよい
submit(event.text)
}
}
}
private fun submit(text: String) {
// 安全に処理できる
}
}
ここで何が起きているか
この ViewModel の中には、明確な境界 があります。
境界の「手前」
FormUiStateonTextChangedonSubmitClicked- if による検証
ここは 未判断の世界 です。
境界の「向こう側」
FormEvent.SubmithandleEventsubmit
ここは 判断済みの世界 です。
イベントが「境界を越えた通知」になる理由
この構造では、
- 判断は UI 層側で完了し
- Event がその結果を表し
- ViewModel は「判断済みの世界」だけを扱う
という分離が成立します。
つまり、
イベントとは、 「判断が完了し、境界を越えてよい状態になった」 という事実の通知 なのです。
なぜ sealed class が効くのか
sealed class を使うことで、
- 判断が終わっていない状態を Event にできない
- 種類が増えたらコンパイルエラーで気づける
- 「何が起こりうるか」が構造として閉じる
という効果が得られます。
これはまさに、
判断を構造で表現している 状態です。
この章のまとめ
- 本書におけるイベントは、UI イベントではない
- イベントとは、「判断が完了したこと」を表す構造
- Event が生成できた時点で、境界を越えてよい
- sealed class は、「判断済み」を型で保証する
第8章で述べた抽象的な定義は、 このように コードとして具体化 されます。