第1回:なぜComposeでつまずくのか?UI宣言の本質

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

Jetpack Composeは「シンプルで直感的」と言われます。

しかし、実際に使い始めると、多くの初学者が“わからない再コンポーズ”、“意図しないUI更新”、“State地獄”に悩まされます。

本シリーズの第1回では、「なぜComposeは難しく感じるのか?」その根本原因を“宣言的UIの本質”から紐解き、若手エンジニアが最初に理解すべき基礎をわかりやすく解説します。

 

なぜComposeは難しく感じるのか?

Androidのビューシステム(XML + View)は「命令型」でした。

ボタンを作る
→ テキストを設定する
→ enable を false にする
→ 背景色を変える
→ フラグも更新する

という、手続き・変化の積み重ねでUIを作っていました。

つまり、

「どの瞬間に何を変えるか」をプログラマが細かく制御する世界

です。

一方、Composeは完全に別物です。

Composeは
『今、このUIはどうあるべきか?』
を宣言する仕組みです。

宣言的 UI の思考法が切り替わらないと、必ず詰む

若手エンジニアがハマる典型例はこれです。

  • ViewModel 内の値を更新したのに UI が変わらない
  • remember の動作が理解できない
  • 意図しない再コンポーズでパフォーマンス低下
  • State がどこにあるか不明でバグが発生
  • フローの collect を間違えて無限ループに陥る

これらの根底にあるのは、

旧来の「命令型UI思考」でComposeを書こうとしている

という点です。

 

宣言的UIとは何か?

宣言的UIを理解する最も良い方法は、
「Composeは関数=UIのスナップショットを返す仕組み」
と捉えることです。

@Composable
fun Greeting(name: String) {
    Text("Hello $name")
}

これは「Hello xxx を表示する」という“現時点の UI の定義”であり、時間経過も状態変化も含んでいないただの関数です。

UI は State によって自動的に変わる

var text by remember { mutableStateOf("初期値") }
Text(text)

この2行の意味はこうです。

  • State が変われば
  • Compose が再コンポーズし
  • UI が自動的に更新される

プログラマが UI を「更新しない」世界なのです。

 

若手がハマる典型的なミス(実例)

ここでは、実際によくある失敗コードを紹介します。

ダメな例:State を UI の外に置いてしまう

var count = 0

@Composable
fun Counter() {
    Button(onClick = { count++ }) {
        Text("Count: $count")
    }
}

一見正しそうですが、count が普通の変数なので
UI は絶対に更新されません。

これは宣言的UIの根本誤解です。

修正版:State を Composable が監視できる形にする

@Composable
fun Counter() {
    var count by remember { mutableStateOf(0) }

    Button(onClick = { count++ }) {
        Text("Count: $count")
    }
}

このコードの意味はこうです。

  • remember の State は Compose が監視する
  • count が変われば自動で再コンポーズ
  • UI が正しく変わる

宣言的UIの最初の壁は「状態は監視される形で持つ」ことを理解できるかどうかです。

 

宣言的UIが本当に難しい理由:状態の置き場所問題

若手が次に必ずぶつかるのがこれです。

「State はどこに置くべき?」

これはUIフレームワーク全般で最も難しいテーマの1つです。

NG例

  • すべて ViewModel に持たせる(太りすぎる)
  • すべて Composable に持たせる(再利用性ゼロ)
  • どこかに rememberSaveable を付けたが理由はわからない

これを避けるために覚えておくべき原則があります。

 

Compose の「状態管理原則」3つ

① UI が持つべき状態と、アプリが持つべき状態は分ける

  • 入力中のテキスト → UI状態
  • 中身のデータ → アプリ状態

これは Flutter も React も同じです。

② UI 状態は「最も近い共通の親」に置くのが基本

例:複数TextFieldを跨ぐ状態 → 親Composableに

これは State hoisting の基本ルールです。

③ そもそも状態を持つ必要があるか考える

例:

val filteredList = items.filter { it.isActive }

これは「状態」ではないので State にしてはいけない。

State 化すると逆にバグが増えます。

 

よくある問題:無限リコンポーズ

(第3回で詳しくやりますが、軽く触れておきます)

❌ ダメな例:毎回新しいオブジェクトを生成

@Composable
fun Bad() {
    val list = listOf("A", "B", "C") // 毎回新しいList
    ListDisplay(list)
}

これ、毎回 list が変わったと判断され、下層が無限リコンポーズする可能性があります。

✔ 修正版:remember でメモ化

@Composable
fun Good() {
    val list = remember { listOf("A", "B", "C") }
    ListDisplay(list)
}

宣言的UIでは、「値が同じでもオブジェクトが変わると別物扱い」になるため、データの寿命管理が極めて重要になります。

 

Compose を理解するための“思考の置き換え”

宣言的UIを理解するには思考の転換が必要です。

❌ 古いUI思考

  • UIを操作する
  • ビューを探して変更する
  • findViewById
  • setText
  • setColor
  • 変更を覚えておく

✔ Compose 思考

  • 状態を変える
  • UIは状態から「勝手に」作られる
  • UIの変更は一切命令しない
  • 再コンポーズは友達

この転換が本当に難しい。
でも、ここを超えれば Compose は圧倒的な生産性を発揮します。

 

まとめ:Compose の本質を理解すれば強くなれる

本記事で伝えたかった一番大事なポイントはこれです。

Composeとは「状態」だけをいじり、UIは宣言するだけの世界である

この理解さえあれば、

  • Stateの置き場所
  • 再コンポーズ
  • Modifier地獄
  • ViewModel連携

なども徐々に整理できるようになります。

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