Jetpack Composeの宣言的UIモデル
UIが崩れる原因の多くは手続き的な更新漏れ。状態から出力を再計算する宣言的モデルと再コンポーズの仕組みを理解すれば、無駄な描画も不具合も潰せる。
- 1.Composeは命令的にUIを書き換えるのではなく、状態からUIツリーを再計算する宣言的モデル。再コンポーズは関数の再実行であり、画面の破棄・再構築ではない。
- 2.Stateの変更はComposeランタイムに読み取り位置を記録させ、値が変わった箇所だけを再スケジュールする。影響範囲を絞り込む単位はComposable関数そのもの。
- 3.安定性(stability)が保証された引数は等価性チェックでスキップでき、再コンポーズの伝播を止められる。SwiftUIのView再評価とも設計思想は共通する。
命令的UIから宣言的UIへ
従来のAndroid View階層は、状態が変わるたびに開発者がsetTextやsetVisibilityを呼んで差分を手動反映する命令的モデルでした。更新箇所を呼び忘れれば表示は状態と食い違ったままになります。Jetpack Composeはこの手続きを逆転させます。UIを「状態を受け取って出力を返す関数」として書き、状態が変わったら関数を再実行して新しいUIツリーを再計算する。呼び出す側は「今の状態ではこう見えるべきだ」を宣言するだけで、差分の反映はランタイムの責務になります。
@Composable
fun Counter(count: Int, onIncrement: () -> Unit) {
Button(onClick = onIncrement) {
Text("count: $count")
}
}
countが変わるたびにこの関数は丸ごと再実行されますが、実際に画面へ描き直されるのは値が変化したTextだけです。関数の再実行と実描画の更新は別レイヤーの話であり、ここを混同すると最適化の仕組みが理解できません。
再コンポーズ:破棄ではなく再実行
再コンポーズ(recomposition)とは、Composable関数を新しい引数・新しい状態値で再実行することです。重要なのは、これがView階層の破棄・再生成ではない点です。Composeランタイムは前回のコンポジション結果を保持しており、再実行後の出力をスロットテーブルと呼ばれる内部構造上の前回値と比較し、変化がない部分の実描画(レイアウト・描画・入力ツリーの更新)を省略します。
ランタイムは再コンポーズを部分木単位で、必要な回数だけ、必要な順序で実行してよいという契約になっています。Composable関数は副作用を持たず何度呼ばれても結果が変わらないこと(冪等性)が前提であり、関数内でグローバル変数を書き換えるような副作用はLaunchedEffectやSideEffectのような専用APIの外で行うべきではありません。
Stateと読み取り位置の追跡
再コンポーズをどこから始めるかを決めるのがStateオブジェクトです。remember { mutableStateOf(...) }で作られたStateは、値の読み取りが発生した瞬間、Composeランタイムに「このComposableはこのStateを読んでいる」という依存関係を記録させます。これはスナップショットシステム(Snapshot State)によって実現されており、State.valueへの読み取りアクセスがトラッキングのフックになっています。
値が更新されると、ランタイムはそのStateを読んでいたComposable関数だけを「無効(invalid)」としてマークし、次のフレームで無効化された関数だけを再コンポーズの対象にします。読んでいない関数は影響を受けません。したがって、状態を保持する位置をどのComposable関数に置くかが、再コンポーズの影響範囲を直接左右します。大きな親Composableで状態を持つと、値が変わるたびに親ごと再コンポーズの候補になり、状態はできるだけ実際に使う末端のComposableへ寄せる、あるいはderivedStateOfで読み取り単位を絞るのが定石です。
@Composable
fun Parent() {
var query by remember { mutableStateOf("") }
// queryを読むのはSearchFieldだけにする
SearchField(query = query, onChange = { query = it })
ResultList() // queryを読まないので再コンポーズされない
}
スキップ最適化と安定性(stability)
Composeランタイムは、Composable関数を再コンポーズする前に「入力引数が前回と実質的に同じなら呼び出し自体を丸ごとスキップできるか」を判定します。これがスキップ最適化です。判定にはコンパイラプラグインが関数の引数型に付与する**安定性(stability)**の情報が使われます。
型が「安定」とみなされるには、次の条件を満たす必要があります。
| 条件 | 内容 |
|---|---|
| 等価性の一貫性 | equalsの結果がインスタンスの生存期間中に変わらない(=publicなプロパティがvalで不変、またはMutableStateのような監視可能な仕組みで変更が通知される) |
| 変更の通知 | プロパティが変わればComposeランタイムに通知が届く(Stateの読み取り追跡に乗る) |
| 再帰的な安定性 | すべてのプロパティの型も安定である |
data classで全プロパティがvalかつ安定な型であれば自動的に安定と推論されます。一方、varを含むクラスや、標準のList/Map(可変実装を許すインターフェースであるため)は不安定と推論され、スキップ対象から外れます。不安定な引数を持つComposableは、呼び出し元が再コンポーズされるたびに無条件で再実行され、下流に無駄な再コンポーズを伝播させます。これが「Listを使うと再コンポーズが減らない」とよく言われる理由で、対策としてkotlinx.collections.immutableのImmutableListのような、コンパイラが安定と認識できる型に置き換えます。
Composeコンパイラは-P plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=...のようなオプションでコンポーズ可能性レポートを出力でき、各パラメータが安定(Stable)か不安定(Unstable)かをビルド時に確認できます。疑わしいComposableはまずこのレポートで安定性を確認するのが実務的な近道です。
SwiftUIとの発想の共通点
Jetpack ComposeとSwiftUIは別々の言語・ランタイムですが、設計思想の骨格はよく似ています。どちらも「UIは状態の純粋な関数として宣言し、状態が変われば関数(Composable関数/View.body)を再評価し、実際の描画は差分だけを反映する」というモデルを取ります。SwiftUIのViewがbodyを再評価するトリガーは@Stateや@Observableの変更で、ComposeのState読み取り追跡と役割は同じです。また、SwiftUIも構造体であるViewの同一性・差分検出(Equatable実装や識別子)によって再描画範囲を絞り込む点は、Composeの安定性チェックによるスキップ最適化と対応します。両者とも「宣言する側は差分計算を意識しない、フレームワーク側が最小限の更新に落とし込む」という責務分離が核心であり、命令的UIツールキットからの移行者がまず慣れるべきなのはこの責務の移し替え方です。
再コンポーズは「関数の再実行」であって画面の再生成ではないこと、影響範囲はStateの読み取り位置で決まること、スキップ判定は安定性(stability)に基づき不変なval構成やImmutableな型で保証しやすいことの3点は頻出です。可変な標準コレクションを引数に取ると安定性が崩れ最適化が効かなくなる点も具体例として説明できるようにしておくとよいでしょう。
まとめ
Jetpack Composeの宣言的UIモデルは、状態から出力を再計算するという単純な原則の上に、再コンポーズの範囲をStateの読み取り追跡で絞り込み、さらに安定性に基づくスキップ最適化で無駄な再実行そのものを消す、という二段構えの最適化で成り立っています。状態をどこに置くか、引数の型が安定かどうかを意識するだけで、パフォーマンス特性は大きく変わります。この発想はSwiftUIをはじめとする他の宣言的UIフレームワークとも共通しており、一度原理を掴めば知識は移植可能です。
モバイル開発 Article
Jetpack Composeの宣言的UIモデルを実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
Jetpack Compose
比較で見る軸
難易度: advanced / カテゴリ: モバイル開発 / タグ数: 5
導入後に効く点
Stateの変更はComposeランタイムに読み取り位置を記録させ、値が変わった箇所だけを再スケジュールする。影響範囲を絞り込む単位はComposable関数そのもの。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- モバイル開発
- タグ数
- 5
判断チェックリスト
- 自社の用途が「Jetpack Compose / Android」に近いか確認する。
- 強みである「Composeは命令的にUIを書き換えるのではなく、状態からUIツリーを再計算する宣言的モデル。再コンポーズは関数の再実行であり、画面の破棄・再構築ではない。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。