第3回:MutableState と不変データ構造の本当の関係

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

Jetpack Compose を始めたばかりの若手エンジニアが必ずぶつかる壁――それが「値を更新したのに UI が変わらない」問題です。

原因は単純で、しかし非常に奥が深い。

今回のテーマは MutableState と Immutable(不変)データ構造の真の関係

これを理解すると、Compose での状態管理が一気に視界良好になります。

第1章 UI が更新されない…その原因は“変更していない”から

まず典型的な若手エンジニアの悩みから紹介します。

よくあるシナリオ

  • List を mutableStateOf(list) で保持
  • ボタンを押したら list に .add() して更新
  • でも UI は変わらない

実際のコードはこんな感じです。

var items by remember { mutableStateOf(listOf("A", "B", "C")) }

Button(
    onClick = {
        // やりがちなミス:リストに add() している
        (items as MutableList).add("D")
    }
) {
    Text("追加")
}

LazyColumn {
    items(items) { item ->
        Text(item)
    }
}

現象

  • ボタンを押しても UI に “D” が追加されない
  • デバッグすると items に D が入っている
  • しかし画面が更新されない

そして若手は必ずこう言います:

「state の中身は変わってるのに、なんで更新されないの?」

第2章 MutableState が監視しているのは“参照の変化”だけ

答えはシンプルです。

MutableState が監視しているもの

  • 値そのものではなく、参照の変更

つまり:

  • 内部の中身が変わっても、
  • 参照が変わらなければ「変化がない」とみなす

このため、MutableList.add() のような 参照を変えずに中身だけ書き換える操作 を行うと、Compose 側は変化に気付けません。

だから UI は更新されないのです。

第3章 Compose が不変データ構造を推奨する理由

Compose の公式ドキュメントは、明確にこう言っています。

「UI の状態は不変であることが望ましい」

これは哲学ではなく、技術的な合理性に基づいた推奨です。

理由①:変更の瞬間が明確になる

MutableList に add() すると “いつ変更したか” が分かりません。
しかし Immutable List(不変リスト)を扱うと:

items = items + "D"

というように、常に新しいインスタンスが生まれます

+ 演算子は「既存の List を変更する演算子」ではなく、「要素を追加した 新しい List を返す関数」として定義されているため、新しいインスタンスが作成されることになります。

つまり:

✔ 参照が必ず変わる

→ Compose が確実に「変化した」と判断できる
→ UI が確実に更新される

これが大きなメリットです。

理由②:バグの温床を回避できる

MutableList を使うと:

  • UI から見えないところで意図せず変更される
  • 複数の Composable が同じリストを共有してバグる
  • 破壊的変更により、履歴が一切残らない

結果的に “再現不可能なバグ” が多発します。

Immutable の場合:

  • データは毎回新しく生成されるため
  • 常に「現在の状態」が明確
  • 元のデータが汚染されない

結果としてコードベースが安定します。

理由③:Compose の Recomposition モデルに適している

Compose は “スナップショットシステム” を持ち、再描画を最小限に抑えるように最適化しています。

Immutable データを使うことで:

  • 差分検知が高速
  • 古いデータが消えることなく保持される
  • “変更点” が明確で、Recomposition が効率的に行われる

Compose を最も高速に動かせるのは Immutable データ

第4章 正しい書き方(Immutable パターン)

先ほどの UI が更新されないコードを修正すると、こうなります。

var items by remember { mutableStateOf(listOf("A", "B", "C")) }

Button(
    onClick = {
        items = items + "D"
    }
) {
    Text("追加")
}

LazyColumn {
    items(items) { item ->
        Text(item)
    }
}

ポイント

  • items + "D" は新しいリストを生成する
  • items = に新しい参照がセットされる
  • Compose は変化を検知して UI を更新する

これが 正しい Compose の状態更新パターン です。

第5章:データクラスにも不変性が重要

Immutable はリストに限りません。

❌ 間違った例(mutable な値を持つ)

data class User(
    var name: String, // ← var は危険
)

✔ 正しい例(すべて val)

data class User(
    val name: String
)

変更したい場合は copy を使う

var user by remember { mutableStateOf(User("Taro")) }

Button(
    onClick = {
        user = user.copy(name = "Hanako")
    }
) {
    Text("変更")
}

こうすることで:

  • データ構造そのものが新しく作られ
  • “参照の変更” が発生し
  • UI が確実に更新される

第6章:若手がやりがちなアンチパターン

アンチパターン①:MutableList を state に突っ込む

var list by remember { mutableStateOf(mutableListOf<String>()) }

これは99%バグります。

アンチパターン②:state を外部から破壊的に書き換える

fun addItem(list: MutableList<String>) {
    list.add("X")
}

外から書き換えてしまうと、“いつ UI が更新されるべきか” を Compose が判断できなくなります。

アンチパターン③:ViewModel の中で mutable なオブジェクトを共有

private val _items = mutableListOf<Item>() // ← 危険

Compose の更新タイミングと ViewModel 内の副作用がズレて、“表示がおかしい” が頻発します。

第7章:推奨されるベストプラクティス

✔ 状態は Immutable にする

  • List → listOf()
  • Map → mapOf()
  • Data class → 全項目 val

✔ 更新は常に「新しいインスタンスを生成」する

  • List → items = items + newItem
  • Data class → copy()

✔ MutableState は参照の変更だけを監視するという仕組みを理解する

これが Compose の見えない前提です。

✔ ViewModel の UIState は Immutable 一択

data class UiState(
    val items: List<Item> = emptyList(),
    val isLoading: Boolean = false,
)

ViewModel からは常に新しい UiState を emit することで、UI の挙動が安定します。

終章:Immutable を理解した瞬間、Compose が一気に“手懐けられる”

Jetpack Compose の最大の強みは “再現性の高さ” です。
その根幹にある思想が 「UI 状態は不変であるべき」 という哲学。

MutableState は参照の変化を監視するだけなので:

  • データは Immutable
  • 更新は new インスタンス
  • 変更は copy()
  • List は items + value

これらを守るだけで、Compose のバグのほとんどは姿を消します。

もしあなたが次に誰かを教える立場になるなら、
ぜひこう言ってあげてください。

「Compose は不変データと一緒に使うと最強になるよ」

これが、今回の核心です。

タイトルとURLをコピーしました