Slide 1

Slide 1 text

Strong Skipping Mode によって recompositionはどう変わったの か DroidKaigi.onCompletion { 2024@Online } mikan(一瀬喜弘)

Slide 2

Slide 2 text

自己紹介

Slide 3

Slide 3 text

目的 Strong Skipping Mode を有効にすることで、不安定なパラメ ータに依存するComposable関数のrecompositionに、どのよ うな違いが生じるのか検証してみた 注意 コード例は検証用のものなので、およそプロダクションコー ドで利用するようなものにはなってません

Slide 4

Slide 4 text

Strong Skipping Mode とは TL;DR 不安定なパラメータに依存しているComposable関数も skippableがつくようになった

Slide 5

Slide 5 text

Strong Skipping Mode を有効にする 方法はいろいろある 1. compose-runtime:1.7.0 2. kotlin 2.0.20 3. compose compiler にオプションを渡す // build.gradle.kts // 1. tasks.withType() { compilerOptions.freeCompilerArgs.addAll( "-P", "plugin:androidx.compose.compiler.plugins.kotlin:experimentalStrongSkipping=true", ) } composeCompiler { // 2. before kotlin 2.0.20 enableStrongSkippingMode = true // 3. after kotlin 2.0.20 featureFlags = setOf( ComposeFeatureFlag.StrongSkipping.disabled() // 無効にする書き方

Slide 6

Slide 6 text

検証用コード @Composable fun Names( names: List, ) { Row( horizontalArrangement = Arrangement.spacedBy(8.dp), ) { names.forEach { Text(it) } } }

Slide 7

Slide 7 text

検証用コード class UnstableUserClass( val names: List, ) @Composable fun User(user: UnstableUserClass) { Names(user.names) }

Slide 8

Slide 8 text

検証用コード data class UnstableUserDataClass( val names: List, ) @Composable fun User(user: UnstableUserDataClass) { Names(user.names) }

Slide 9

Slide 9 text

検証用コード @Stable class StableUserClass( val names: List, ) @Composable fun User(user: StableUserClass) { Names(user.names) }

Slide 10

Slide 10 text

検証用コード @Immutable class ImmutableUserClass( val names: List, ) @Composable fun User(user: ImmutableUserClass) { Names(user.names) }

Slide 11

Slide 11 text

検証用コード @Stable data class StableUserDataClass( val names: List, ) @Composable fun User(user: StableUserDataClass) { Names(user.names) }

Slide 12

Slide 12 text

検証用コード @Immutable data class ImmutableUserDataClass( val names: List, ) @Composable fun User(user: ImmutableUserDataClass) { Names(user.names) }

Slide 13

Slide 13 text

@Composable fun Users( unstableUserClass: UnstableUserClass, unstableUserDataClass: UnstableUserDataClass, stableUserClass: StableUserClass, immutableUserClass: ImmutableUserClass, stableUserDataClass: StableUserDataClass, immutableUserDataClass: ImmutableUserDataClass, names: List, modifier: Modifier = Modifier, ) { Column(modifier) { User(unstableUserClass) User(unstableUserDataClass) User(stableUserClass) User(immutableUserClass) User(stableUserDataClass) User(immutableUserDataClass) Names(names) } }

Slide 14

Slide 14 text

@Composable fun MainScreen1( count: Int, onChangeCount: (Int) -> Unit, modifier: Modifier = Modifier, ) { val names = mutableListOf("mikan") val unstableUserClass = UnstableUserClass(names) val unstableUserDataClass = UnstableUserDataClass(names) val stableUserClass = StableUserClass(names) val immutableUserClass = ImmutableUserClass(names) val stableUserDataClass = StableUserDataClass(names) val immutableUserDataClass = ImmutableUserDataClass(names) Column(modifier) { Text("Count: $count") Button({ names += "mikan" onChangeCount(count + 1) }) { Text("Increment") } Users( unstableUserClass, unstableUserDataClass, stableUserClass, immutableUserClass, stableUserDataClass, immutableUserDataClass, names, ) } }

Slide 15

Slide 15 text

@Composable fun MainScreen2( count: Int, onChangeCount: (Int) -> Unit, modifier: Modifier = Modifier, ) { val names = remember { mutableListOf("mikan") } // positional memoized by remember val unstableUserClass = UnstableUserClass(names) val unstableUserDataClass = UnstableUserDataClass(names) val stableUserClass = StableUserClass(names) val immutableUserClass = ImmutableUserClass(names) val stableUserDataClass = StableUserDataClass(names) val immutableUserDataClass = ImmutableUserDataClass(names) Column(modifier) { Text("Count: $count") Button({ names += "mikan" onChangeCount(count + 1) }) { Text("Increment") } Users( unstableUserClass, unstableUserDataClass, stableUserClass, immutableUserClass, stableUserDataClass, immutableUserDataClass, names, ) } }

Slide 16

Slide 16 text

@Composable fun MainScreen3( count: Int, onChangeCount: (Int) -> Unit, modifier: Modifier = Modifier, ) { val names = remember { mutableListOf("mikan") } val unstableUserClass = remember { UnstableUserClass(names) } val unstableUserDataClass = remember { UnstableUserDataClass(names) } val stableUserClass = remember { StableUserClass(names) } val immutableUserClass = remember { ImmutableUserClass(names) } val stableUserDataClass = remember { StableUserDataClass(names) } val immutableUserDataClass = remember { ImmutableUserDataClass(names) } Column(modifier) { Text("Count: $count") Button({ names += "mikan" onChangeCount(count + 1) }) { Text("Increment") } Users( unstableUserClass, unstableUserDataClass, stableUserClass, immutableUserClass, stableUserDataClass, immutableUserDataClass, names, ) } }

Slide 17

Slide 17 text

MainScreen1 すべてrecompositionした。recompositionの前後で見た目は変わらず → recompositionの度にすべての変数が再割り当てされているから → StableUserDataClassとImmutableUserDataClassがrecompositionした理由について、この説明だと違和感 が残る

Slide 18

Slide 18 text

MainScreen2 (mutableListにrememberをつけたやつ) @Stable と @Immutable を​ つけた data class だけスキップした。​ recompositionした​ ものは​ 描画が​ 更新された

Slide 19

Slide 19 text

MainScreen2 (mutableListにrememberをつけたやつ) @Composable fun MainScreen2( count: Int, onChangeCount: (Int) -> Unit, modifier: Modifier = Modifier, ) { val names = remember { mutableListOf("mikan") } // recomposition時: キャッシュが存在するので再割り当ては発生しない val unstableUserClass = UnstableUserClass(names) // UnstableUserClass(["mikan", "mikan"]) → UnstableUserClass(["mikan", "mikan"]) val unstableUserDataClass = UnstableUserDataClass(names) // UnstableUserDataClass(["mikan", "mikan"]) → UnstableUserDataClass(["mikan", "mikan"]) val stableUserClass = StableUserClass(names) // StableUserClass(["mikan", "mikan"]) → StableUserClass(["mikan", "mikan"]) val immutableUserClass = ImmutableUserClass(names) // ImmutableUserClass(["mikan", "mikan"]) → ImmutableUserClass(["mikan", "mikan"]) val stableUserDataClass = StableUserDataClass(names) // StableUserDataClass(["mikan", "mikan"]) → StableUserDataClass(["mikan", "mikan"])

Slide 20

Slide 20 text

MainScreen2 (mutableListにrememberをつけたやつ) @Composable fun MainScreen2( count: Int, onChangeCount: (Int) -> Unit, modifier: Modifier = Modifier, ) { val names = remember { mutableListOf("mikan") } // recomposition時: キャッシュが存在するので再割り当ては発生しない val unstableUserClass = UnstableUserClass(names) // skippableでないのでrecompositionする val unstableUserDataClass = UnstableUserDataClass(names) // skippableでないのでrecompositionする val stableUserClass = StableUserClass(names) // skippableだが、再割り当てによって参照が変わっているのでrecompositionする val immutableUserClass = ImmutableUserClass(names) // skippableだが、再割り当てによって参照が変わっているのでrecompositionする val stableUserDataClass = StableUserDataClass(names) // skippableであり、equals比較において同じとみなされるのでrecompositionしない

Slide 21

Slide 21 text

MainScreen3 (変数すべてにrememberをつけたやつ) @Stable と @Immutable をつけたものについてはスキップした。recompositionしたものは描画が更新された

Slide 22

Slide 22 text

MainScreen3 (変数すべてにrememberをつけたやつ) @Composable fun MainScreen3( count: Int, onChangeCount: (Int) -> Unit, modifier: Modifier = Modifier, ) { val names = remember { mutableListOf("mikan") } // recomposition時: キャッシュされているため再割り当ては発生しない val unstableUserClass = remember { UnstableUserClass(names) } // recomposition時: キャッシュされているため再割り当ては発生しない val unstableUserDataClass = remember { UnstableUserDataClass(names) } // recomposition時: キャッシュされているため再割り当ては発生しない val stableUserClass = remember { StableUserClass(names) } // recomposition時: キャッシュされているため再割り当ては発生しない val immutableUserClass = remember { ImmutableUserClass(names) } // recomposition時: キャッシュされているため再割り当ては発生しない val stableUserDataClass = remember { StableUserDataClass(names) } // recomposition時: キャッシュされているため再割り当ては発生しない

Slide 23

Slide 23 text

MainScreen3 (変数すべてにrememberをつけたやつ) @Composable fun MainScreen3( count: Int, onChangeCount: (Int) -> Unit, modifier: Modifier = Modifier, ) { val names = remember { mutableListOf("mikan") } // recomposition時: キャッシュされているため再割り当ては発生しない val unstableUserClass = remember { UnstableUserClass(names) } // skippableでないのでrecompositionする val unstableUserDataClass = remember { UnstableUserDataClass(names) } // skippableでないのでrecompositionする val stableUserClass = remember { StableUserClass(names) } // skippableであり、再割り当ては発生していないのでスキップ val immutableUserClass = remember { ImmutableUserClass(names) } // skippableであり、再割り当ては発生していないのでスキップ val stableUserDataClass = remember { StableUserDataClass(names) } // skippableであり、再割り当ては発生していないのでスキップ

Slide 24

Slide 24 text

Strong Skipping Mode MainScreen1 すべてrecompositionした: 変化なし

Slide 25

Slide 25 text

Strong Skipping Mode MainScreen2 (mutableListにrememberをつけたやつ) @Stable と @Immutable をつけた data class および listを単純に渡しているものについてはスキップした → listはキャッシュが使われるのでスキップしたと考えられる

Slide 26

Slide 26 text

Strong Skipping Mode MainScreen3 (変数すべてにrememberをつけたやつ) すべてスキップした → すべてskippableになり、再割り当ては発生していないのでスキップしたものと考えられる

Slide 27

Slide 27 text

Strong Skipping Mode 気になった点 @Composable fun Names( names: List, ) { Row( horizontalArrangement = Arrangement.spacedBy(8.dp), ) { names.forEach { Text(it) } } } Names(listOf("mikan")) // skip

Slide 28

Slide 28 text

まとめ Strong Skipping Mode を有効化することで、不安定な型であっても同値であればrecompositionをスキップ するようになった 公式ドキュメントには、不安定な型については === で比較するとあったが、 == で比較しているように 見える これまでたまたま描画が更新できていた箇所が、Strong Skipping Mode を有効化によって更新しなくなる 可能性がある

Slide 29

Slide 29 text

ご清聴ありがとうございました