第8回:UI State を正規化しないと破綻する理由

― Boolean が増えた瞬間、UI は壊れ始める ―

※ChatGPTを使用して記事を作成しています。

はじめに

Jetpack Compose を使っていると、最初はこう思います。

「状態を持たせれば UI は書ける」

そして気づくと、ViewModel にこんな State が生まれます。

data class UiState(
    val isLoading: Boolean = false,
    val isError: Boolean = false,
    val isEmpty: Boolean = false,
    val isSuccess: Boolean = false
)

一見すると分かりやすそうです。
しかしこれは、UI State 破綻の第一歩です。

この記事では、

  • UI State が破綻する理由
  • 正規化されていない State の危険性
  • Compose での正しい State 設計

失敗例 → 問題点 → 修正版 の順で解説します。

UI State が破綻する瞬間

上記の State には、致命的な問題があります。

問題①

組み合わせとして成立しない状態が存在する

  • isLoading = true
  • isError = true

ロード中でエラーとは?

  • isEmpty = true
  • isSuccess = true

空なのに成功?

👉 コード上は可能だが、UIとして意味がない

問題②

「どれを表示すればいいか」が UI 側に漏れる

when {
    state.isLoading -> Loading()
    state.isError -> Error()
    state.isEmpty -> Empty()
    state.isSuccess -> Content()
}
  • 優先順位は?
  • 同時に true だったら?
  • 誰が責任を持つ?

👉 UIが状態解釈を始めた時点で設計は崩壊

なぜ Boolean が増えがちなのか

理由は単純です。

  • 最初は1画面1状態
  • 要件追加で1フラグ追加
  • 「念のため」false にしておく
  • 修正漏れ

👉 人間は組み合わせを管理できない

これは Compose の問題ではなく、状態設計の問題です。

正規化とは何か?

正規化とは、

「同時に成立しない状態を、構造で表現すること」

つまり、

  • 不正な状態を作れない
  • 解釈の余地がない
  • UI が迷わない

正解①:sealed class による正規化

✅ 正規化された UI State

sealed class UiState {
    object Loading : UiState()
    object Empty : UiState()
    data class Success(val items: List<Item>) : UiState()
    data class Error(val message: String) : UiState()
}

UI 側

when (state) {
    UiState.Loading -> Loading()
    UiState.Empty -> Empty()
    is UiState.Success -> Content(state.items)
    is UiState.Error -> Error(state.message)
}

✔ 同時成立不可
✔ UI の分岐が明確
✔ 意味のない状態が存在しない

正規化の副作用:State が減る

Boolean 地獄から抜けると、

  • 状態数が減る
  • UI 分岐が減る
  • バグが減る

👉 コードは長くなるが、設計はシンプル

よくある誤解

「柔軟性が下がるのでは?」

答えは NO です。

例えば「リフレッシュ中」を追加する場合。

❌ Boolean 方式

val isRefreshing: Boolean

→ 全組み合わせを再検討

✅ 正規化方式

sealed class UiState {
    object Loading
    object Refreshing
    ...
}

新しい状態を1つ追加するだけ

データと状態は分ける

UI State に 生データを詰め込みすぎる のも破綻の元です。

❌ 失敗例

data class UiState(
    val items: List<Item>?,
    val error: Throwable?,
    val isLoading: Boolean
)
  • null 判定地獄
  • if の嵐

✅ 正しい分離

sealed class UiState {
    object Loading
    object Empty
    data class Content(val items: List<Item>)
    data class Error(val message: String)
}

👉 null を状態にしない

State 正規化の設計チェックリスト

次の質問に YES が多いなら危険です。

  • Boolean が3つ以上ある
  • null を意味として使っている
  • UI 側に when/if が散らばっている
  • ViewModel 以外で状態解釈している

正規化されていると Compose が活きる理由

Compose は、

  • 入力が明確
  • 出力が一意
  • 差分判定しやすい

という前提で設計されています。

👉 正規化された State は Compose と相性が良い。

設計の本質

UI State 設計のゴールはこれです。

「今この画面は、どんな状態か?」を
1文で説明できること

それができない State は、ほぼ確実に破綻します。

まとめ

  • Boolean が増えたら危険信号
  • 同時成立しない状態は構造で表現
  • 正規化は「制限」ではなく「安全装置」
  • UI が迷わない State を作る

UI State を正規化できるようになると、

  • Compose が楽になる
  • バグが減る
  • 設計の説明ができる
タイトルとURLをコピーしました