Jetpack Compose:再Composeとスキップ

投稿日:  更新日:

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とスキップの様子

周期的(1/60秒)に起動される描画処理は、3つのフェーズComposition/Layout/Drawingにより構成されます。

周期的に実行される描画処理

「再Compose」は「フェーズCompositionを実行すること」です。もっと具体的に言えば「Composable関数を実行すること」です。その結果、表示が更新されます。

ただし、入力(引数の値)が変更になり、UI要素の状態(表示)が変わるComposable関数に限ります。それ以外は、再Composeをスキップします。

再Compose-表示が変わるとき
再Compose-実行とスキップ

これは、高い表示パフォーマンス(シームレスな画面変化・アニメーション)維持するために、周期内に描画処理を終わらせたいからです。周期内に終わらない場合はてフレーム落ちになります。

ちなみに、再Compose(フェーズComposition)が実行された場合は、その後のフェーズLayoutとDrawingの実行が必須となります。ですので、スキップはパフォーマンスの維持に最も効果的です。

サンプルアプリ:スライドショー
        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)
                    }
                }
            }
        }
@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")
        }
    }
}

※描画処理とフェーズについては「Composeによるアプリ画面の描画」を参照
※Layout Inspectorについては「Layout Inspectorで再Composeを確認」を参照

スポンサーリンク

再Composeのスケジューリング

UI要素の状態(表示)に関与する変数を監視して、値が変更されたら参照しているComposable関数を再Composeの対象として記録します。

再Composeのターゲット

この「再Composeの対象として記録する」ことを、ドキュメントは「再Composeをスケジューリングする」と表現しています。描画処理は周期的(1/60秒間隔)であることから、再Composeの実行は次の描画処理まで待たされることになります。

再Composeのスケジューリング

スポンサーリンク

再Composeの起点

描画処理が起動されると、スケジューリングされたComposable関数が実行されます。この関数が再Composeの起点です。

再Composeの起点

起点以下に子Composable関数が存在すれば、その関数も実行されます。この点は、普通の関数の実行と変わりません。

ただし、子Composable関数に対して、再Composeとスキップの判定が行われます。

スポンサーリンク

再Composeとスキップの判定

再Composeとスキップの判定は入力(引数の値)を見て判定されます。

スキップの判定

UIツリーを辿ってComposable関数が実行される際に、Composition中の関数と引数の同値性(equalsメソッドを使用)を比較します。

スキップの判定

同値性を比較した結果、全ての引数が等しければ、この関数はスキップされます。入力が同じなので、UI要素の状態(表示)が変わらないと判断できるからです。それ以外は実行(再Compose)されます。

安定した型の引数

スキップの判定で使用される引数は「安定した型」が求められます。

「安定した型」はJetpack Composeのシステムが暗黙的に判断するものと、明示的に判断するものがあります。

安定した型条件
暗黙的プリミティブ型Boolean
Int
Long
Float
Char
など
文字列型String
関数型(ラムダ式)
ユーザ定義のクラス引数をvalで宣言(不変)プロパティも安定した型
ユーザ定義のデータクラス
明示的ユーザ定義のクラス引数をvarで宣言(可変)
@Stable付
ユーザ定義のデータクラス
※明示的:アノテーション@Stableの付加で「安定した型」になる

「安定した型」ではない引数を持つComposable関数は、スキップの判定は行われずに、必ず再Composeが実行されるので、注意が必要です。

安定した型
「安定した型」とは、「インスタンスが作成された後、プロパティ値が変更できない型」のことです。

例えば、Int型の整数4のインスタンスは、プロパティを整数5へ変更できません。ですので、安定しています。

しかし、varで宣言されたプロパティを持つユーザ定義のクラスは「インスタンスが作成された後、プロパティ値が変更できる型」です。

例えば、PersonalInfoクラスがプロパティnameを持つとします。PersonalInfo型のインスタンスは、nameを”tanaka”から”suzuki”へ変更できます。ですので、安定していません。

ちなみに、List型は安定しています。でも、内部へ格納するデータが可変なので、完全に安定していると言えません。「安定しているが可変である型」と表現されます。

※「安定した型」の詳細はドキュメント「入力が変化していない場合にスキップする)」を参照してください。

スポンサーリンク

例:プリミティブ&文字列&ラムダを引数に持つ

プリミティブ・文字列・ラムダは「安定した型」です。ですので、これらを引数に持つComposable関数はスキップ判定が行われます。

setContent {
            var _dummy = remember { mutableStateOf(0) }
            var _boolean1 = remember { true }
            var _boolean2 = remember { false }
            var _int1 = remember { 100 }
            var _int2 = remember { 200 }
            var _string1 = remember { "ABC" }
            var _string2 = remember { "XYZ" }
            var _lambda1: ()->String = remember { fun() = "Apple" }
            var _lambda2: ()->String = remember { fun() = "Orange" }
            MyApplicationTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    Column {  // Columnはinline関数 -> Surfaceへ展開される
                        SampleBoolean(state = _boolean1)
                        SampleBoolean(state = _boolean2)
                        SampleInt(state = _int1)
                        SampleInt(state = _int2)
                        SampleString(state = _string1)
                        SampleString(state = _string2)
                        SampleLambda(state = _lambda1)
                        SampleLambda(state = _lambda2)
                        // ↓↓Surface階層に再Composeをスケジューリングためのダミー↓↓
                        val __dummy = _dummy.value
                        Button(onClick = {_dummy.value++}) {
                            Text(text = "Recompose ${__dummy}")
                        }
                    }
                }
            }
            // 別スレッドで状態を更新
            val _coroutineScope = rememberCoroutineScope()
            _coroutineScope.launch(Dispatchers.Default) {
                delay(10000)
                _boolean2 = true
                _int2 = 300
                _string2 = "###"
                _lambda2 = fun() = "Banana"
                Log.i(TAG, "Changed state !")
            }
        }
@Composable
fun SampleBoolean(state: Boolean) {
    Text(text = "Boolean State = ${state}")
}
@Composable
fun SampleInt(state: Int) {
    Text(text = "Int     State = ${state}")
}
@Composable
fun SampleString(state: String) {
    Text(text = "String  State = ${state}")
}
@Composable
fun SampleLambda(state: ()->String) {
    Text(text = "String  State = ${state()}")
}

プリミティブ&文字列&ラムダ

スポンサーリンク

例:データクラスを引数に持つ

引数をvar(可変なプロパティ)で宣言したデータクラスは「安定した型」ではありません。ですので、このデータクラスを引数に持つComposable関数は必ず再Composeの対象になります。

        setContent {
            var _dummy = remember { mutableStateOf(0) }
            var _person1 = remember { PersonalInfo("Yamada", 18) }
            var _person2 = remember { PersonalInfo("Saitou", 23) }
            var _person3 = remember { PersonalInfo("Tanaka", 11) }
            MyApplicationTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    Column {
                        Member(info = _person1)
                        Member(info = _person2)
                        Member(info = _person3)
                        // ↓↓Surface階層に再Composeをスケジューリングためのダミー↓↓
                        val __dummy = _dummy.value
                        Button(onClick = {_dummy.value++}) {
                            Text(text = "Recompose ${__dummy}")
                        }
                    }
                }
            }
            // 別スレッドで状態を更新
            val _coroutineScope = rememberCoroutineScope()
            _coroutineScope.launch(Dispatchers.Default) {
                delay(10000)
                _person1.age = 19
                _person2 = PersonalInfo("Saitou", 24)
                Log.i(TAG, "Changed state !")
            }
        }
@Composable
fun Member(info: PersonalInfo) {
    Row {
        Text(text = "Member = ${info.name}")
        Text(text = "(${info.age})")
    }
}
//@Stable
data class PersonalInfo( var name: String = "", var age: Int = 0)

データクラス(@Stable無)

しかし、アノテーション@Stableの付加し「安定した型」であることを明示的に示せば、スキップの判定が行われます。

@Stable
data class PersonalInfo( var name: String = "", var age: Int = 0)

データクラス(@Stable有)

データクラスは引数にプロパティを列記すると、プロパティの同値性を比較するequals関数が自動生成されます。

equals( )関数の詳細は「Kotlin:classおよびdata classのequals( )関数の動作」を参照してください。

スポンサーリンク

関連記事:

プロジェクトのビルドで「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はAndroidシステムの新たなUIフレームワークです。従来のViewシステムと、アプリ画面の描画の仕組みが異なります。 このJetpack 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 ...
スポンサーリンク