他のほとんどの UI ツールキットと同様に、Compose は複数の異なるフェーズを介したフレームをレンダリングします。たとえば、Android View システムには、測定、レイアウト、描画の 3 つの主要なフェーズがあります。Compose はよく似ていますが、開始時にコンポジションという重要な追加のフェーズがあります。
Compose のドキュメントでは、Compose の思想と State や Jetpack Compose でコンポジションについて説明しています。
フレームの 3 つのフェーズ
Compose には、次の 3 つの主要なフェーズがあります。
- コンポジション: 表示する UI。Compose はコンポーズ可能な関数を実行し、UI の説明を作成します。
- レイアウト: UI を配置する場所。このフェーズは、測定と配置の 2 段階で構成されます。レイアウト要素は、レイアウト ツリー内の各ノードについて、自身と子要素を 2D 座標で測定して配置します。
- 描画: レンダリングの方法。UI 要素は、キャンバス(通常はデバイス画面)に描画されます。

このフェーズの順序は基本的に同じで、データはコンポジションからレイアウト、描画へと一方向に移行し、単方向データフローとしてフレームが生成されます。子のコンポジションが親のレイアウト フェーズに依存している場合、BoxWithConstraints
、LazyColumn
、LazyRow
は例外です。
コンセプト上、これらの各フェーズはすべてのフレームで発生しますが、パフォーマンスを最適化するために、Compose はこれらのすべてのフェーズで同じ入力から同じ結果を計算する反復処理を回避します。以前の結果を再利用できる場合、Compose はコンポーズ可能な関数の実行をスキップします。必要がない場合、Compose UI でツリー全体の再レイアウトや再描画は行われません。Compose は、UI の更新に必要な最小限の作業のみを実行します。Compose は異なるフェーズ内で状態の読み取りを追跡するため、この最適化が可能です。
フェーズを理解する
このセクションでは、コンポーザブルに対して 3 つの Compose フェーズが実行される仕組みについて詳しく説明します。
合成
コンポジション フェーズでは、Compose ランタイムがコンポーズ可能な関数を実行し、UI を表すツリー構造を出力します。この UI ツリーは、次のフェーズに必要なすべての情報を含むレイアウト ノードで構成されています。次の動画をご覧ください。
図 2. コンポジション フェーズで作成される UI を表すツリー。
コードと UI ツリーのサブセクションは次のようになります。

これらの例では、コード内の各コンポーズ可能な関数は、UI ツリー内の単一のレイアウト ノードにマッピングされます。より複雑な例では、コンポーザブルにロジックと制御フローを含めることができ、異なる状態に応じて異なるツリーを生成できます。
レイアウト
レイアウト フェーズでは、Compose はコンポジション フェーズで生成された UI ツリーを入力として使用します。レイアウトノードのコレクションには、2D 空間内の各ノードのサイズと位置を決定するために必要なすべての情報が含まれています。
図 4. レイアウト フェーズにおける UI ツリー内の各レイアウト ノードの測定と配置。
レイアウト フェーズでは、次の 3 ステップのアルゴリズムを使用してツリーがトラバースされます。
- 子を測定する: ノードは、子が存在する場合に子を測定します。
- Decide own size: これらの測定値に基づいて、ノードは独自のサイズを決定します。
- 子を配置: 各子ノードは、ノード自身の位置を基準にして配置されます。
このフェーズの終了時に、各レイアウト ノードには次のものが含まれます。
- 割り当てられた幅と高さ
- 描画する x、y 座標
前のセクションの UI ツリーを思い出してください。
このツリーの場合、アルゴリズムは次のように動作します。
Row
は、子であるImage
とColumn
を測定します。Image
が測定されます。子がないため、自身のサイズを決定し、そのサイズをRow
に報告します。- 次に
Column
が測定されます。まず、自身の子(2 つのText
コンポーザブル)を測定します。 - 最初の
Text
が測定されます。子がないため、自身のサイズを決定し、そのサイズをColumn
に報告します。- 2 番目の
Text
が測定されます。子がないため、自身のサイズを決定してColumn
に報告します。
- 2 番目の
Column
は、子の測定値を使用して自身のサイズを決定します。子要素の最大幅と子要素の高さの合計を使用します。Column
は、子を自身に対して相対的に配置し、垂直方向に上下に配置します。Row
は、子の測定値を使用して自身のサイズを決定します。この場合、子の最大高さと子の幅の合計が使用されます。次に、子を配置します。
各ノードは 1 回だけアクセスされたことに注意してください。Compose ランタイムでは、UI ツリーを 1 回だけ通過してすべてのノードを測定して配置するため、パフォーマンスが向上します。ツリー内のノード数が増加すると、ツリーの走査にかかる時間も線形に増加します。一方、各ノードが複数回アクセスされた場合、走査時間は指数関数的に増加します。
描画
描画フェーズでは、ツリーが上から下に再度トラバースされ、各ノードが順番に画面に描画されます。
図 5. 描画フェーズでは、画面にピクセルが描画されます。
前の例を使用すると、ツリー コンテンツは次のように描画されます。
Row
は、背景色などのコンテンツを描画します。Image
は自身を描画します。Column
は自身を描画します。- 1 つ目と 2 つ目の
Text
はそれぞれ描画されます。
図 6. UI ツリーとその描画表現。
状態の読み取り
前述のフェーズのいずれかで snapshot state
の value
を読み取ると、Compose は value
の読み取り時に行われた処理を自動的に追跡します。この追跡により、Compose は状態の value
が変更されたときにリーダーを再実行できるようになり、Compose の状態に関するオブザーバビリティの基礎となります。
状態は、通常は mutableStateOf()
を使用して作成され、その後 value
プロパティに直接アクセスする方法または Kotlin プロパティのデリゲートを使用する方法のいずれかでアクセスされます。詳細については、コンポーザブル内の状態をご覧ください。このガイドでは、「状態の読み取り」とは、これらの同等のアクセス方法のいずれかを指します。
// State read without property delegate. val paddingState: MutableState<Dp> = remember { mutableStateOf(8.dp) } Text( text = "Hello", modifier = Modifier.padding(paddingState.value) )
// State read with property delegate. var padding: Dp by remember { mutableStateOf(8.dp) } Text( text = "Hello", modifier = Modifier.padding(padding) )
プロパティ デリゲートでは、「getter」関数と「setter」関数を使用して、状態の value
へのアクセスと更新を行います。これらの getter 関数と setter 関数は、プロパティが作成されたときではなく、プロパティを参照したときにのみ呼び出されます。そのため、前述の 2 つの方法は同等です。
読み取り状態が変更されたときに再実行できるコードの各ブロックは、再起動スコープです。Compose は、状態 value
の変化を追跡し、さまざまなフェーズでスコープを再起動します。
段階的な状態の読み取り
前述のように、Compose には 3 つの主要なフェーズがあり、Compose は各フェーズ内で読み取られる状態を追跡します。これにより、Compose は影響を受ける UI の要素ごとに作業を実行する必要がある特定のフェーズにのみ通知できます。
以降のセクションでは、各フェーズと、そのフェーズ内で状態の値が読み込まれたときの処理について説明します。
フェーズ 1: コンポジション
@Composable
関数またはラムダブロック内の状態の読み取りはコンポジションに影響を与え、場合によっては後続のフェーズに影響します。状態の value
が変更されると、recomposer は、その状態の value
を読み取るすべてのコンポーズ可能な関数の再実行をスケジュール設定します。入力が変更されていない場合、ランタイムがコンポーズ可能な関数の一部またはすべてをスキップすることを決定する場合があります。詳しくは、入力が変更されていない場合にスキップするをご覧ください。
コンポジションの結果に応じて、Compose UI はレイアウトと描画のフェーズを実行します。コンテンツが同じで、サイズとレイアウトが変更されない場合は、これらのフェーズをスキップできます。
var padding by remember { mutableStateOf(8.dp) } Text( text = "Hello", // The `padding` state is read in the composition phase // when the modifier is constructed. // Changes in `padding` will invoke recomposition. modifier = Modifier.padding(padding) )
フェーズ 2: レイアウト
レイアウト フェーズは、測定と配置の 2 つのステップで構成されます。測定のステップでは、Layout
コンポーザブルに渡される測定ラムダ、LayoutModifier
インターフェースの MeasureScope.measure
メソッドなどが実行されます。配置のステップでは、layout
関数のプレースメント ブロック、Modifier.offset { … }
のラムダブロックなどの関数が実行されます。
各ステップでの状態の読み取りは、レイアウトと描画のフェーズに影響します。状態の value
が変更されると、Compose UI がレイアウト フェーズをスケジュール設定します。また、サイズや位置が変更された場合も、描画フェーズが実行されます。
var offsetX by remember { mutableStateOf(8.dp) } Text( text = "Hello", modifier = Modifier.offset { // The `offsetX` state is read in the placement step // of the layout phase when the offset is calculated. // Changes in `offsetX` restart the layout. IntOffset(offsetX.roundToPx(), 0) } )
フェーズ 3: 描画
描画コード中の状態の読み取りは、描画フェーズに影響します。一般的な例には、Canvas()
、Modifier.drawBehind
、Modifier.drawWithContent
などがあります。状態の value
が変更されると、Compose UI は描画フェーズのみを実行します。
var color by remember { mutableStateOf(Color.Red) } Canvas(modifier = modifier) { // The `color` state is read in the drawing phase // when the canvas is rendered. // Changes in `color` restart the drawing. drawRect(color) }
状態の読み取りを最適化する
Compose がローカライズされた状態の読み取りの追跡を行う際、適切なフェーズで各状態を読み取ることで、実行される作業量を最小限に抑えることができます。
次の例をご覧ください。この例では、オフセット修飾子を使用して最終のレイアウト位置をオフセットする Image()
を作成し、ユーザーがスクロールしたときのパララックス効果を実現しています。
Box { val listState = rememberLazyListState() Image( // ... // Non-optimal implementation! Modifier.offset( with(LocalDensity.current) { // State read of firstVisibleItemScrollOffset in composition (listState.firstVisibleItemScrollOffset / 2).toDp() } ) ) LazyColumn(state = listState) { // ... } }
このコードは機能しますが、最適なパフォーマンスが得られません。記述されているように、このコードは firstVisibleItemScrollOffset
状態の value
を読み取り、Modifier.offset(offset: Dp)
関数に渡します。ユーザーがスクロールすると、firstVisibleItemScrollOffset
の value
が変化します。学習したように、Compose はあらゆる状態の読み取りを追跡するため、読み取りコードの再起動(ここでは再呼び出し)が可能になります。この例では、Box
のコンテンツです。
これは、コンポジションのフェーズ内で読み取られる状態の例です。これは必ずしも悪いことではなく、実際には再コンポジションのベースであり、データ変更によって新しい UI が出力できるようになります。
重要なポイント: この例は最適ではありません。コンポーザブル コンテンツ全体がすべてのスクロール イベントによって再評価され、測定、配置されてから最後に描画されるためです。スクロールごとに Compose フェーズがトリガーされます。ただし、表示されているコンテンツは変更されておらず、位置のみが変更されています。状態の読み取りを最適化して、レイアウト フェーズを再度トリガーできます。
ラムダによるオフセット
オフセット修飾子の別のバージョンとして Modifier.offset(offset: Density.() -> IntOffset)
を使用できます。
このバージョンはラムダ パラメータを取ります。ここで、結果のパラメータがラムダブロックによって返されます。これを使用するようにコードを更新します。
Box { val listState = rememberLazyListState() Image( // ... Modifier.offset { // State read of firstVisibleItemScrollOffset in Layout IntOffset(x = 0, y = listState.firstVisibleItemScrollOffset / 2) } ) LazyColumn(state = listState) { // ... } }
では、なぜパフォーマンスが向上したのでしょうか?修飾子に提供するラムダブロックは、レイアウト フェーズ(具体的にはレイアウト フェーズの配置ステップ中)で呼び出されます。つまり、firstVisibleItemScrollOffset
状態がコンポジション中に読み取られることはなくなります。Compose は状態の読み取りのタイミングを追跡するため、この変更により、firstVisibleItemScrollOffset
の value
が変更された場合、Compose はレイアウトと描画のフェーズを再開するだけで済みます。
もちろん、多くの場合、コンポジション フェーズでの状態の読み取りは絶対に不可欠です。そのような場合でも、状態の変更をフィルタリングすることで再コンポジションの回数を最小限に抑えることができます。詳細については、derivedStateOf
: 1 つ以上の状態オブジェクトを別の状態に変換するをご覧ください。
再コンポジション ループ(循環型フェーズの依存関係)
このガイドでは、Compose のフェーズは常に同じ順序で呼び出され、同じフレーム内で逆方向に進む方法はないと説明しました。ただし、複数のフレーム間でコンポジション ループに入るアプリは禁止されていません。以下の例で考えてみましょう。
Box { var imageHeightPx by remember { mutableStateOf(0) } Image( painter = painterResource(R.drawable.rectangle), contentDescription = "I'm above the text", modifier = Modifier .fillMaxWidth() .onSizeChanged { size -> // Don't do this imageHeightPx = size.height } ) Text( text = "I'm below the image", modifier = Modifier.padding( top = with(LocalDensity.current) { imageHeightPx.toDp() } ) ) }
この例では、垂直方向の列を実装し、上部に画像、その下にテキストを配置しています。Modifier.onSizeChanged()
を使用して画像の解決済みサイズを取得し、テキストで Modifier.padding()
を使用して画像の下方にシフトさせます。Px
から Dp
への不自然な変換は、すでにコードになんらかの問題があることを意味しています。
この例での問題は、1 つのフレーム内の「最終」レイアウトに到達しないことです。このコードでは複数のフレームの発生に依存しているため、不要な処理が行われ、UI がユーザーの画面上でジャンプします。
最初のフレームの構成
最初のフレームのコンポジション フェーズでは、imageHeightPx
は最初は 0
です。したがって、コードは Modifier.padding(top = 0)
を含むテキストを提供します。後続のレイアウト フェーズで onSizeChanged
修飾子のコールバックが呼び出され、imageHeightPx
が画像の実際の高さに更新されます。Compose は、次のフレームの再コンポジションをスケジュール設定します。ただし、現在の描画フェーズでは、更新された imageHeightPx
値がまだ反映されていないため、テキストはパディング 0
でレンダリングされます。
2 番目のフレームの構成
Compose は、imageHeightPx
の値の変更によってトリガーされた 2 番目のフレームを開始します。このフレームのコンポジション フェーズでは、状態は Box
コンテンツ ブロック内で読み取られます。テキストには、画像の高さと正確に一致するパディングが提供されるようになりました。レイアウト フェーズでは、imageHeightPx
が再度設定されますが、値が同じままであるため、再コンポジションはスケジュールされません。
このサンプルは不自然に感じられるかもしれませんが、次の一般的なパターンには注意が必要です。
Modifier.onSizeChanged()
、onGloballyPositioned()
などのレイアウト オペレーション- 一部の状態を更新する
- その状態をレイアウト修飾子(
padding()
、height()
など)への入力として使用する - 繰り返される可能性がある
上のサンプルの修正では、適切なレイアウト プリミティブを使用します。上の例は、Column()
で実装できますが、より複雑な例でカスタムされたものが必要な場合は、カスタム レイアウトの記述が必要になります。詳しくは、カスタム レイアウト ガイドをご覧ください。
ここでの一般的な原則は、互いに測定して配置すべき複数の UI 要素について、信頼できる情報源を 1 つだけ保持することです。適切なレイアウト プリミティブを使用したり、カスタム レイアウトを作成したりすると、複数の要素間の関係を調整する際の信頼できる情報源として、最低限に共有された親を利用できます。動的な状態を導入すると、原則に反することになります。
あなたへのおすすめ
- 注: JavaScript がオフになっている場合はリンクテキストが表示されます
- 状態と Jetpack Compose
- リストとグリッド
- Jetpack Compose で Kotlin を使用する