Jetpack ComposeはAndroidシステムの新たなUIフレームワークです。従来のViewシステムと、アプリ画面の描画の仕組みが異なります。
このJetpack Composeによるアプリ画面の描画について、仕組みの大枠をまとめます。
※環境:Android Studio Giraffe | 2022.3.1 Patch 1
Kotlin 1.8.10
Compose Compiler 1.4.3
フレーム処理
Androidは周期的(1/60秒が目標)にフレームメモリーへ1枚の画面データを描画(展開)しています。これにより、画面の更新やシームレスなアニメーションを表現しています。
ここでは、この処理を「フレーム処理」と呼ぶことにします。
フレーム処理の前半は描画処理を実行し、描画コマンドを作成します。これはUIスレッドが担当します。※描画処理については後述
( これ以降は推測ですが… )
フレーム処理の後半は描画コマンドに従い、フレームメモリーへ画面データを描画(展開)します。これはRenderスレッドが担当します。通常、この処理を行うのはOpen GL ESで操作されるGPUです。

Jetpack ComposeのUIツリーはActivity#setContent( )関数によりActivityへ関連付けられます。この時、UIツリーの接続先はViewシステムのルートです。つまり、根底はViewシステムと同じ構成になっていると考えられます。

※Layout Inspectorの一部を抜粋

※Layout Inspectorの一部を抜粋
描画処理
描画処理は3つのフェーズで構成され、上から下へ一方向に処理されます。

3フェーズ(Composition, Layout, Drawingの順番)の実行が基本です。
しかし、表示の状態(変更の有無)によって、フェーズ毎に実行したりスキップしたりします。※フェーズの実行については後述
フェーズ:Composition
Composable関数を実行することで、関数(UI要素)のツリー構造をデータ化し、メモリーへ格納します。この処理をComposeといい、データをCompositionといいます。

※Composable関数:@Composableの付いた関数
※ComposeコンパイラがComposable関数をComposeするバイトコードへ変換
フェーズ:Layout
Compositionの結果に従って、UI要素のサイズ(w,h)と位置(x,y)を算出します。

フェーズ:Drawing
Layoutの結果に従って、UI要素をCanvasへ描画します。

フェーズの実行
周期的に行われる描画処理において、表示が変更されないUI要素(Composable関数)の再描画は無駄な処理です。ですので、表示が変更されたUI要素のみを再描画する手法が取られます。
しかも、この再描画の判断はフェーズ毎に行われます。つまり、各フェーズは表示が変更された場合に実行され、変更されない場合はスキップします。
実行の条件
フェーズの実行の条件は図のようになります。
各フェーズの状態の読み取り対象が異なる点に注意してください。

フローチャートに示した通り、フェーズ:Compositionが実行されると、フェーズ:LayoutとDrawingの実行は必須となります。
Jetpack Composeのドキュメントに「再Compose」という言葉が登場します。これは、再描画でフェーズ:Compositionを実行する事と同意です。さらに、再描画で描画処理(3フェーズ全て)を実行する事と同意です。
実行の例
例はフェーズの実行を見える形に表現したサンプルです。
アプリの起動から10秒後に状態が更新されます。これにより表示が変更されて、条件に合ったフェーズが実行されます。
- 上段の写真 –> フェーズ:Drawingを実行
- 中段の写真 –> フェーズ:Layoutを実行
- 下段の写真 –> フェーズ:Compositionを実行
val _photos = arrayOf(
Pair("Azami", R.drawable.azami),
Pair("Bike", R.drawable.bike),
Pair("Leaf", R.drawable.leaf1),
Pair("Leaf", R.drawable.leaf2)
)
setContent {
val _dummy = remember { mutableStateOf(0) }
val _flag = mutableStateOf(false)
val _photo0 = _photos[0].second
val _modifier0 = Modifier.drawWithContent {
drawContent()
if(_flag.value) // _flagの変更 -> Drawingをスケジューリング
drawRoundRect(color = Color.Red, style = Stroke(3.0f))
}
val _photo1 = _photos[1].second
val _modifier1 = Modifier.offset {
if(_flag.value) // _flagの変更 -> Layoutをスケジューリング
IntOffset(10.dp.roundToPx(), 0)
else
IntOffset.Zero
}
var _photo2 = _photos[2].second
val _modifier2 = Modifier
MyApplicationTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Photo(_photo0, modifier = _modifier0)
Spacer(modifier = Modifier.size(10.dp))
Photo(_photo1, modifier = _modifier1)
Spacer(modifier = Modifier.size(10.dp))
Photo(_photo2, modifier = _modifier2)
Spacer(modifier = Modifier.size(10.dp))
// ↓↓Surface階層に再Composeをスケジューリングためのダミー↓↓
val __dummy = _dummy.value
Button(onClick = {_dummy.value++}) {
Text(text = "Recompose ${__dummy}")
}
}
}
}
// 別スレッドで状態を更新
val _coroutineScope = rememberCoroutineScope()
_coroutineScope.launch(Dispatchers.Default) {
delay(10000)
_flag.value = true
_photo2 = _photos[3].second // _photo2の変更 -> Compositionをスケジューリング
}
}
@Composable
fun Photo(@DrawableRes id: Int, modifier: Modifier = Modifier) {
Image(
painter = painterResource(id), contentDescription = null,
modifier = modifier.size(140.dp, 105.dp)
)
}
フェーズ:DrawingとLayoutは再Compose(再Composition)で無いため、Layout Inspectorで観測(カウント値)されません。

※Layout Inspectorの表示は「Jetpack Compose:Layout Inspectorで再Composeを確認」を参照
関連記事:
