※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 は不変データと一緒に使うと最強になるよ」
これが、今回の核心です。

