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(フェーズComposition)が実行された場合は、その後のフェーズLayoutとDrawingの実行が必須となります。ですので、スキップはパフォーマンスの維持に最も効果的です。
※描画処理とフェーズについては「Composeによるアプリ画面の描画」を参照
※Layout Inspectorについては「Layout Inspectorで再Composeを確認」を参照
再Composeのスケジューリング
UI要素の状態(表示)に関与する変数を監視して、値が変更されたら参照しているComposable関数を再Composeの対象として記録します。
この「再Composeの対象として記録する」ことを、ドキュメントは「再Composeをスケジューリングする」と表現しています。描画処理は周期的(1/60秒間隔)であることから、再Composeの実行は次の描画処理まで待たされることになります。
再Composeの起点
描画処理が起動されると、スケジューリングされたComposable関数が実行されます。この関数が再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 data class PersonalInfo( var name: String = "", var age: Int = 0)
データクラスは引数にプロパティを列記すると、プロパティの同値性を比較するequals関数が自動生成されます。
equals( )関数の詳細は「Kotlin:classおよびdata classのequals( )関数の動作」を参照してください。
関連記事: