Jetpack Compose:Composeによるアプリ画面の描画

投稿日:  更新日:

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システムと同じ構成になっていると考えられます。

Jetpack ComposeViewシステム
Jetpack ComposetのsetContent接続先
※Layout Inspectorの一部を抜粋
ViewシステムのsetContent接続先
※Layout Inspectorの一部を抜粋
スポンサーリンク

描画処理

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

Composeのフェーズ処理

3フェーズ(Composition, Layout, Drawingの順番)の実行が基本です。

しかし、表示の状態(変更の有無)によって、フェーズ毎に実行したりスキップしたりします。※フェーズの実行については後述

サンプルアプリ:スライドショー
スライドショー
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
val _photos = arrayOf(
    Pair("Azami", R.drawable.azami),
    Pair("Bike", R.drawable.bike),
    Pair("Leaf", R.drawable.leaf1),
    Pair("Leaf", R.drawable.leaf2)
)
 
setContent {
    MyApplicationTheme {
        Surface(
            modifier = Modifier.fillMaxSize(),
            color = MaterialTheme.colorScheme.background
        ) {
            var _index by remember { mutableStateOf(1) }
            val _prev = { if(_index > 0) _index-- }
            val _next = { if(_index < 3) _index++ }
            SlideShow {
                val _photo = painterResource(_photos[_index].second)
                val _title = _photos[_index].first
                PhotoPanel(photo =_photo, title = _title)
                Spacer(modifier = Modifier.size(10.dp))
                Control(prev = _prev, next = _next)
            }
        }
    }
}
スライドショー
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Composable
fun SlideShow(content: @Composable () -> Unit) {
    Column( // inline関数なので展開->階層は残らない
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center,
        modifier = Modifier.padding(10.dp)
    ) {
        content()
    }
}
 
@Composable
fun PhotoPanel(photo: Painter, title: String) {
    Column( // inline関数なので展開->階層は残らない
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(text = "【 ${title} 】", fontSize = 24.sp)
        Spacer(modifier = Modifier.size(10.dp))
        Image(painter = photo, contentDescription = title)
    }
}
 
@Composable
fun Control(prev: () -> Unit, next: () -> Unit, ) {
    Row { // inline関数なので展開->階層は残らない
        CombinedButton(onClick = prev, indication = null) {
            Text(text = "Prev")
        }
        CombinedButton(onClick = next, indication = null) {
            Text(text = "Next")
        }
    }
}

フェーズ:Composition

Composable関数を実行することで、関数(UI要素)のツリー構造をデータ化し、メモリーへ格納します。この処理をComposeといい、データをCompositionといいます。

フェーズComposition

※Composable関数:@Composableの付いた関数
※ComposeコンパイラがComposable関数をComposeするバイトコードへ変換

フェーズ:Layout

Compositionの結果に従って、UI要素のサイズ(w,h)と位置(x,y)を算出します。

フェースLayout

フェーズ:Drawing

Layoutの結果に従って、UI要素をCanvasへ描画します。

フェーズDrawing

スポンサーリンク

フェーズの実行

周期的に行われる描画処理において、表示が変更されないUI要素(Composable関数)の再描画は無駄な処理です。ですので、表示が変更されたUI要素のみを再描画する手法が取られます。

しかも、この再描画の判断はフェーズ毎に行われます。つまり、各フェーズは表示が変更された場合に実行され、変更されない場合はスキップします。

実行の条件

フェーズの実行の条件は図のようになります。

各フェーズの状態の読み取り対象が異なる点に注意してください。

フェーズのスキップの条件

フローチャートに示した通り、フェーズ:Compositionが実行されると、フェーズ:LayoutとDrawingの実行は必須となります。

Jetpack Composeのドキュメントに「再Compose」という言葉が登場します。これは、再描画でフェーズ:Compositionを実行する事と同意です。さらに、再描画で描画処理(3フェーズ全て)を実行する事と同意です。

実行の例

例はフェーズの実行を見える形に表現したサンプルです。

アプリの起動から10秒後に状態が更新されます。これにより表示が変更されて、条件に合ったフェーズが実行されます。

  • 上段の写真 –> フェーズ:Drawingを実行
  • 中段の写真 –> フェーズ:Layoutを実行
  • 下段の写真 –> フェーズ:Compositionを実行
フェーズの実行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
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をスケジューリング
    }
}
フェーズの実行
1
2
3
4
5
6
7
@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を確認」を参照

スポンサーリンク

関連記事:

プロジェクトのビルドで「Something went wrong while checking for version compatibility between the Compose Compiler and the Kotlin Compiler.」とメッセージを吐き、エラーになる場合があります。 既存のプロジェクトを新しくリリースされたAndroid Studioでビルドした場合に頻発します。 先日、「Giraffe|2022.3.1」がリリース(2023.07)されて、早速、ビルドをしたら発生しました。 その対処方法を説明します。 ※環境:Android Studio Giraffe | 2022.3.1 ...
表示の変わらないUI要素(Composable関数)に対して行われる再Composeは無駄な処理です。 ですので、極力排除したいところですが、表示が変わらないため、画面上からの判断が難しくなっています。 このようなとき、Layout Inspectorを利用すると、無駄な再Composeを見つけ出すことが出来ます。 ※環境:Android Studio Giraffe | 2022.3.1 Patch 1 ...
Jetpack Composeは描画処理の軽量化(消費リソース量の削減)をするために、表示の変更されたUI要素のみを再Composeし、表示の変わらないUI要素をスキップします。これにより、高い表示パフォーマンスを維持しています。 しかし、スキップが正常に行われないとしても、アプリの画面に現れて来ません。なぜなら、同じ表示を無駄に繰り返すことになるからです。 アプリは動くけれど動作が鈍いならば、真っ先に疑うポイントです。不要な再Composeが行われている可能性が考えられます。 これは気付かないうちに蓄積し易い不具合です。ですので、再Composeとスキップについて理解し、予防に努めることをお勧めします。 今回は「再Composeとスキップ」について、まとめます。 ※環境:Android Studio Giraffe | 2022.3.1 Patch 1     Kotlin 1.8.10     Compose Compiler 1.4.3 ...
サンプルアプリを作成して、描画処理(再Compose)の周期を観測してみました。 その結果を紹介します。 ※環境:Android Studio Giraffe | 2022.3.1 Patch 1     Kotlin 1.8.10     Compose Compiler 1.4.3 ...
mutableStateOfはComposable関数ではありません。 ですので、Composable関数内にある必要はなく、どこでも記述できます。 Activityから表示の更新を発行する方法として使えそうです。 ※環境:Android Studio Giraffe | 2022.3.1 Patch 1     Kotlin 1.8.10     Compose Compiler 1.4.3 ...
状態の保持(remenber)はアプリの画面(UI)を制御・管理するために必要な動作です。 再Composeのスケジューリング(mutableStateOf)に並び、Jetpack Composeの重要な技術の一つです。 今回は「再Composeを超えて状態の保持」について、まとめます。 ※環境:Android Studio Giraffe | 2022.3.1 Patch 1     Kotlin 1.8.10     Compose Compiler 1.4.3 ...
再Composeのスケジューリング(mutableStateOf)はアプリのパフォーマンスに直結する動作です。 状態の保持(remember)に並び、Jetpack Composeの重要な技術の一つです。 今回は「再Composeのスケジューリング」について、まとめます。 ※環境:Android Studio Giraffe | 2022.3.1 Patch 1     Kotlin 1.8.10     Compose Compiler 1.4.3 ...
CompositionLocalはUIツリーで発生する状態のバケツリレーを解決してくれます。 また、広く共有したい状態の定義にも適しています。例えば、「ツリーのある階層以下に対して」などと言った場合です。 CompositionLocalについて、まとめます。 ※環境:Android Studio Giraffe | 2022.3.1 Patch 1     Kotlin 1.8.10     Compose Compiler 1.4.3 ...
CompositionLocalはUIツリーにローカルな変数を確保します。 その変数の参照キーはcompositionLocalOf関数(以降、Dynamic側と呼ぶ)によって返されるCompositionLocalインスタンスです。 このインスタンスを返す方法に、もう一つ、staticCompositionLocalOf関数(以降、Static側と呼ぶ)があります。 この両者の違いをまとめます。 ※環境:Android Studio Giraffe | 2022.3.1 Patch 1     Kotlin 1.8.10     Compose Compiler 1.4.3 ...
CompositionLocalはシステムにより提供されているものがあります。 プログラミングに有益で利用頻度の高いものが用意されています。例えば、LocalContextやLocalConfigurationなどです。 ※環境:Android Studio Giraffe | 2022.3.1 Patch 1     Kotlin 1.8.10     Compose Compiler 1.4.3 ...
「Android Studio Giraffe」の作成するプロジェクトは、Jetpack Composeの利用が推奨されます。 今後、Viewシステムに代わり、Jetpack Composeが主流になるようです。 ※環境:Android Studio Giraffe | 2022.3.1 Patch 1     Kotlin 1.8.10     Compose Compiler 1.4.3 ...
「Android Studio Giraffe」の作成するプロジェクトは、Jetpack Composeの利用が推奨されます。 そして、作成されたプロジェクトは、Material Designeに準拠したテーマが指定されます。 ※環境:Android Studio Giraffe | 2022.3.1 Patch 1     Kotlin 1.8.10     Compose Compiler 1.4.3 ...
Android StudioにおけるJetpack Composeプロジェクトは、エディタ上でUIのプレビューが行えます。 Kotlinで記述した画面構成(UIツリー)が視覚的に確認できるので、とても便利です。 さらに、色々な表示条件の設定が行えるので、使いこなせば更に利便性が向上します。 ※この記事の執筆中にドキュメント「コンポーザブルのプレビューで UI をプレビューする」を見つけました。記事はこのドキュメントと重複する部分が多いです。ドキュメントも参考にして下さい。 ※環境:Android Studio Giraffe | 2022.3.1 Patch 3     Kotlin 1.8.10     Compose Compiler 1.4.3 ...
Jetpack Composeが提供する既存のUI要素(Compose UI)は、必ずModifierを引数に持ちます。 このModifierの役割はUI要素へ装飾や機能拡張を追加することですが、裏でアプリ画面の描画処理と密接に関連しており、UI要素よりもシステム側に近い存在です。 理解せずに誤った使い方をすれば、装飾や機能拡張の域を脱してUI要素が表示されないこともあり、思ったようなアプリ画面は望めません。 Modifierについて、まとめます。 ※環境:Android Studio Hedgehog | 2023.1.1     Kotlin 1.8.10     Compose Compiler 1.4.3 ...
スポンサーリンク