CompositionはJetpack Composeの中核プロセスであり、記述的なComposable関数から画面上に表示されるライブUIツリーを構築します。レイアウトがXMLからロードされて不変オブジェクトに変換されるAndroidのViewシステムとは異なり、Compositionは動的システムとして機能します。関数が実行され、メモリ内にスロットを作成し、ノード階層を形成して状態にバインドします。Google Android Developers, 2026によると、Compositionの理解はComposeアプリケーションのパフォーマンス最適化に不可欠です。
主要ポイント
CompositionはComposable関数を実行するプロセスであり、その結果、ユーザーインターフェースがノードツリーとして内部表現されます。このツリーの各ノードは、組み込みコンポーネント(Text、Button、Image)またはユーザー定義のComposable関数の呼び出しに対応します。CompositionはAndroidのViewオブジェクトを直接作成するのではなく、抽象的な記述を構築し、それが後でLayoutおよびDrawingフェーズで処理されます。
Compositionの重要な特徴は再起動可能性です。コンポジション内の各Composable関数は、入力パラメータまたは読み取る状態オブジェクトが変更された場合、いつでも再起動できます。システムはツリー全体を再起動するのではなく、変更されたデータに実際に依存する関数のみを再起動します。
技術的には、CompositionはComposerを通じて管理されます。ComposerはKotlinコンパイラが各Composable関数に埋め込む内部エンジンです。Composerはスロット(位置グループ)に、どの関数がどのパラメータでどの順序で呼び出されたかの情報を書き込みます。後続の呼び出しでは、Composerは新しいデータを保存されたデータと比較し、再起動するかどうかを決定します。
UIツリーの構築プロセスは、ActivityまたはFragment内でsetContentメソッドを呼び出すことから始まります。このメソッドは初期Compositionを作成し、ルートComposable関数の実行を開始します。その後、各ネストされたComposable関数がノードをツリーに追加し、RowにTextとButton、ColumnにImageとCardが含まれるといった階層を形成します。
各ツリーノードは、ソースコード内の位置に基づいた一意の位置キーを受け取ります。このキーは後続の実行時にノードを識別するために使用されます。位置キーは、Composable関数の呼び出し順序が条件に依存してはいけない理由です。ある実行でA -> Bが呼び出され、次の実行でB -> Aが呼び出された場合、Composeは古いノードと新しいノードを照合できません。
@Composable
fun AppScreen() {
Column { // Columnノード(位置1)
HeaderSection() // HeaderSectionノード(位置2)
ContentSection() // ContentSectionノード(位置3)
FooterSection() // FooterSectionノード(位置4)
}
}
@Composable
fun HeaderSection() {
Row { // Rowノード(位置2.1)
Text("タイトル") // Textノード(位置2.2)
Icon(...) // Iconノード(位置2.3)
}
}
この例では、各呼び出しはコード内の順序に基づいた位置を受け取ります。Column(位置1)には3つの子ノード(位置2、3、4)が含まれています。HeaderSectionはさらに2つの子ノード(2.1、2.2、2.3)を追加します。次の再コンポジションでContentSectionがHeaderSectionの前に呼び出された場合、Composerはノードを正しく照合できなくなります。したがって、Composable関数の呼び出し順序は安定している必要があります。
Compositionの状態は、State<T>型のオブジェクトを通じて管理されます。Composable関数が委譲プロパティ(by)を介してStateから値を読み取ると、そのStateへの依存関係を登録します。値が変更されると、このStateを読み取ったすべての関数が次のコンポジションフェーズでの再起動対象としてマークされます。
依存関係登録メカニズムはスナップショットシステムと呼ばれます。Stateが変更されるたびに、スナップショットがすべての変更を記録し、どの関数がそのStateに依存しているかをComposerに通知します。重要なのは、非Composableコード(例:onClickラムダ内)でStateを読み取っても依存関係は登録されないことです。Composable関数内またはコンポジションコンテキストで実行されるラムダ内での読み取りのみが依存関係を登録します。
スナップショットシステムはトランザクション的に機能します。1つのイベント内での複数のState変更が1つのトランザクションに結合され、複数の再コンポジションを防ぎます。これは特にジェスチャー処理において重要です。1つの動きで複数のStateオブジェクトが変更されますが、Composeは1回の再コンポジションのみを実行します。
@Composable
fun StateExample() {
var text by remember { mutableStateOf("Hello") }
var isVisible by remember { mutableStateOf(true) }
Column {
Text(text) // テキストへの依存関係を登録
if (isVisible) { // isVisibleへの依存関係を登録
TextField(value = text, onValueChange = { text = it })
}
Button(onClick = { isVisible = !isVisible }) {
Text(if (isVisible) "非表示" else "表示")
}
}
}
テキストを変更すると、Column、Text、TextFieldのみが再コンポジションされます。Column、Button、isVisible条件は変更されません。この再コンポジションの分離は、画面全体を再描画するシステムに対するComposeの主要な利点です。各Composable関数は、直接読み取るStateオブジェクトのみを追跡します。
CompositionとRecompositionは、Composable関数を実行する2つの異なるモードです。Compositionは画面作成時に1回発生し、システムは初期値ですべてのComposable関数を実行して初期UIツリーを構築します。Recompositionはデータ変更時に複数回発生し、システムは変更された状態に依存する関数のみを再起動します。
モード Compositionはすべてのツリーノードをアクティブにし、各関数にスロットを割り当て、すべての子孫を登録します。Recompositionは選択的に機能します。Composeは各関数の新しいパラメータ値と古いパラメータ値を比較し、変更がない場合は関数を実行しません(スキッピング)。
CompositionとRecompositionはコストが異なります。最初のCompositionは、完全なツリー構築とスロット割り当てが必要なため、より高コストです。Recompositionはより低コストで、特にほとんどの関数が安定している場合、パラメータがequalsで比較され、Composeは呼び出しをスキップします。最大のパフォーマンスを得るには、ほとんどの再コンポジションができるだけ少ない関数に影響するようにする必要があります。
| 特性 | Composition | Recomposition |
|---|---|---|
| 発生タイミング | 1回、最初の表示時 | 複数回、データ変更時 |
| 範囲 | ツリー全体 | 変更された関数のみ |
| パラメータ比較 | 実行されない | スキッピングのために実行 |
| スロット作成 | はい、すべてのスロットが作成される | 新しいノードのみ |
CompositionLocalは、コンポジションツリー全体に暗黙的にデータを渡すメカニズムです。パラメータを直接使用しない何十ものネストされたComposable関数を通じてパラメータを渡す必要がある問題を解決します。明示的なパラメータチェーンの代わりに、データはトップレベルで設定され、CompositionLocal.currentを介して任意のネストされた関数で読み取られます。
MaterialThemeはCompositionLocalの最もよく知られた例です。すべてのComposeコンポーネントは、MaterialTheme.colorScheme、MaterialTheme.typography、MaterialTheme.shapesを介して色、タイポグラフィ、形状を読み取り、パラメータを通じて受け取りません。開発者は、現在のユーザー、ローカライゼーション設定、画面設定などのデータに対して独自のCompositionLocalを作成できます。
重要な制限:CompositionLocalは頻繁に変更されるデータ(スクロール位置、入力フィールドのテキスト)には使用すべきではありません。CompositionLocalを読み取るコンポーネントは値が変更されるたびに再起動するため、動的データには明示的なパラメータまたはStateを使用する方が適切です。CompositionLocalは、めったに変更されないかまったく変更されない設定データに最適です。
val LocalUser = compositionLocalOf<User?> { null }
@Composable
fun AppRoot(user: User, content: @Composable () -> Unit) {
CompositionLocalProvider(LocalUser.provides(user)) {
content()
}
}
@Composable
fun UserAvatar() {
val user = LocalUser.current // 明示的なパラメータなしでの読み取り
AsyncImage(model = user?.avatarUrl, contentDescription = "Avatar")
}
CompositionLocalProviderはスコープを作成し、その内部でLocalUser.currentが指定された値を返します。UserAvatarは中間関数を介して明示的にパラメータを渡すことなくユーザーを読み取ります。これは、データが少数のリーフノードでのみ必要な深い階層で特に価値があります。
よくある質問
Composition中にStateを変更すると、新しい再コンポジションがスケジュールされ、現在のものが完了した後に実行されます。無限ループは発生しません。Composeは各再コンポジションがスナップショットシステムの個別のトランザクションで実行されることを保証します。
最近のデバイスでは、50〜100のComposable関数を持つ画面のCompositionには1〜5ミリ秒かかります。Googleは60fpsのフレームに対して16ミリ秒以内に収めることを推奨しています。Compositionがこの制限を超える場合は、LazyColumnを使用するか、画面をより小さな関数に分割してください。
Compositionの直接的な手動開始はできません。これはComposerによって自動的に管理されます。ただし、CompositionContextにアクセスできる場合は、Stateを変更するか、ルートcomposableでinvalidate()を呼び出すことで再コンポジションを強制できます。
View階層は一度作成されるJavaオブジェクトの不変ツリーです。Compositionはデータが変更されるたびに再構築される仮想ツリーです。Viewは状態をインスタンス変数に保存し、Compositionは関数呼び出し位置にバインドされたスロットに保存します。
Composable関数が呼び出されなくなった場合(例:if条件がfalseになった場合)、Compositionはそのノードを削除し、DisposableEffectのクリーンアップをトリガーします。再表示された場合(ifが再びtrueになった場合)、新しいノードが作成され、古いノードは復元されません。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。