はじめに
Kotlin 2.0.20 より、Compose の Stroig Skipping Mode がデフォルトで有効になりました。
参考 https://developer.android.com/develop/ui/compose/performance/stability/strongskipping
これを機に、再コンポーズと、再コンポーズのスキップについて、理解を深めたく検証を行いました。
安定型、不安定型
再コンポーズのスキップの判定に影響する、「変化」したことが明確な型なのか否かで、安定型・不安定型に分類されます。
- 安定型 (Stable) の例
- プリミティブ型
- String型
- MutableState型
- 不安定型 (Unstable) の例
- Collection
- public プロパティが可変な data class
Collection は、不変の List 型であっても不安定とされます。List インターフェイスなので、実体が MutableList の可能性があり、他の箇所で要素を変更される可能性があるためです。
再コンポーズの発生
- 発生契機は、State の value 変更
- 再コンポーズの対象は、State を参照する変数を持つ Composable 関数を含む下位の Composable 関数
再コンポーズのスキップ
発生契機と混同しがちですが、スキップの条件は単純に値変更では無い。そして Stroig Skipping Mode の有効無効で影響が出るのはこの機能になります。
- スキップは Composable 関数単位で判定される
- Box Row Column などの inline 関数の場合は、親 Composable 関数の再コンポーズに依存する
- Stroig Skipping Mode が無効の場合のスキップ条件
- Composable 関数に渡される全ての引数が、安定型で equals の結果が true の場合
- Stroig Skipping Mode が有効の場合のスキップ条件
- Composable 関数に渡される全ての引数が、安定型で equals の結果が true または、不安定でもオブジェクトイコールの場合
検証
不安定型を引数に渡した時の再コンポーズのスキップの違いを確認する。
検証1 public var パラメータを持つ Data class
data class UnStableData(var done: Boolean = false)
data class StableData(val done: Boolean = false)
不安定型の UnStableData と、安定型の StableData を用意しました。
val unStableData by remember { mutableStateOf(UnStableData()) }
var stableData by remember { mutableStateOf(StableData()) }
unStableData は done プロパティを更新するので mutableStateOf() にする意味は特にありません、雰囲気です。
stableData の方は done プロパティが不変なので、StableData 自体をインスタンス化し直すしか、done = true にする方法がありません。故に安定型となります。再コンポーズ発生契機としても役立ってもらいます。
Button(
onClick = {
unStableData.done = true
stableData = StableData(done = true)
}
) {
ボタンを押したら、それぞれ done = true となるようにします。
@Composable
fun UnStableText(unStableData: UnStableData) {
Text(
text = "UnStableData",
style = MaterialTheme.typography.titleLarge,
color = if (unStableData.done) Color.Red else Color.Black
)
}
@Composable
fun StableText(stableData: StableData) {
Text(
text = "StableData",
style = MaterialTheme.typography.titleLarge,
color = if (stableData.done) Color.Red else Color.Black
)
}
Composable 関数の引数に data class を指定し、done = true で再コンポーズされたなら文字色が赤へ変化するようにします。
UnStableText(unStableData = unStableData)
StableText(stableData = stableData)
Log.d("RecomposeLog", "unStableData: ${unStableData.done}")
Log.d("RecomposeLog", "stableData: ${stableData.done}")
親の Composable 関数から、呼び出しをセットします。再コンポーズ毎に、ログが出るようにしておきます。
結果
Stroig Skipping Mode 左が無効、右が有効です。
ログ上では、ボタンを押した後、unStableData の done も true になっていることがわかります。
unStableData: true
stableData: true
つまり、Stroig Skipping Mode が有効の時は、UnStableText() がスキップされていることがわかります。
検証2 MutableList の参照値を List 変数に渡す
次に、List を用いて検証します。
private val MutableContents = mutableListOf<Int>()
private val Contents: List<Int> = MutableContents
private fun addValueToList(value: Int) {
MutableContents.add(value)
}
このようにすることで、List型の Contents に MutableList が渡ります。危ない記述です。
@Composable
fun ListContent(list: List<Int>) {
LazyColumn(
modifier = Modifier.padding(horizontal = 32.dp),
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.CenterHorizontally
) {
items(list) { value ->
Text(
text = value.toString(),
style = MaterialTheme.typography.titleLarge,
)
}
}
}
ListContent(list = Contents)
Log.d("RecomposeLog", "Contents: $Contents")
List を引数に取り、リスト表示する Composable 関数を記述して、呼び出しをセットします。
ボタンをクリックする度に、MutableContents にランダムな数値が要素追加されるようにします。そして、再コンポーズの契機が欲しいので ボタンをクリックした回数が奇数かの判定 isOdd を State とします。
onClick = {
addValueToList((0..100).random())
isOdd = !isOdd
}
結果
Stroig Skipping Mode 左が無効、右が有効です。
Stroig Skipping Mode が有効の場合、ログではリストに要素が追加されていますが、再コンポーズがスキップされているため、表示はされません。
Contents: [80, 30, 46, 52, 22, 51, 82, 37, 25, 42, 1, 56, 70]
結論
Stroig Skipping Mode を有効にすることによって、Composable 関数の引数が不安定型オブジェクトイコール時に、再コンポーズのスキップを確認できました。
スキップの条件なので、再コンポーズの発生要因と混同しないように注意する。
再コンポーズの発生要因は State の value が更新されて、equals() の結果が false となった時が再コンポーズ発生となります。
デフォルトが有効になったことの背景に、オブジェクト不変性の重要さが十分広まっていることがあるのかと思います。オブジェクトイコールの場合は中身も変わっていないですよね、というコンセンサスなのでしょう。コンセンサスから外れた実装へのフォローより、パフォーマンスの改善の方が優先されるということでしょう。