Slide 1

Slide 1 text

Jetpack Composeの
 パフォーマンスの基本
 Retty アプリチーム 松田
 2022/06/30 


Slide 2

Slide 2 text

自己紹介
 Retty株式会社
 アプリチーム所属
 松田
 
 ● 2020年入社 3年目
 ● やっていること
 ○ 主にAndroid
 ○ たまにiOS
 ○ バックエンドサーバー(Kotlin) 
 アプリエンジニア 特にAndroid / Kotlinバックエンドサーバー に興味があるエンジニア募集中

Slide 3

Slide 3 text

Rettyとは


Slide 4

Slide 4 text

1.Jetpack Composeの基本の動き方
 https://developer.android.com/jetpack/compose/phases 例外) ・BoxWithConstraints ・LazyColumn ・LazyRow 単方向データフローで動 いている

Slide 5

Slide 5 text

1.Jetpack Composeの基本の動き方
 https://developer.android.com/jetpack/compose/phases Treeの作成

Slide 6

Slide 6 text

1.Jetpack Composeの基本の動き方
 https://developer.android.com/jetpack/compose/phases ・サイズ計算 ・座標計算

Slide 7

Slide 7 text

1.Jetpack Composeの基本の動き方
 https://developer.android.com/jetpack/compose/phases いかに効率化するか いかにReCompositionを行わずに Layoutフェーズだけ実行できるか

Slide 8

Slide 8 text

目次
 1. Jetpack Composeの動作フェーズ
 2. Composable
 2.1. Compositionをskipする
 2.2. Rettyで使用している設計例
 2.3. Restart不可能なComposable
 3. Layout
 3.1. Layoutフェーズだけを実行する


Slide 9

Slide 9 text

2.StableとSkippable
 @Composable public fun MainContent( title: String, color: Color, items: List, ) { // ~~~~ } 引数がequalsで比較して同一 であればそのComposable内 のCompositionはスキップされ る ※Composable関数がStable である場合に限る

Slide 10

Slide 10 text

2.StableとSkippable
 val mutableItems = mutableListOf() val items: List = mutableItems mutableItems.add("1") println(items) // [1] 1. Listは外部から変更可能。 → これがStableではないという事 2. 外部から変更された事をComposeは検知できない。 3. なのでskipされずに毎回Compositionが行われる。

Slide 11

Slide 11 text

2.Stableか調べる
 allprojects { gradle.projectsEvaluated { tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { kotlinOptions { def baseDir = "path/to/dir" def outPath = "${baseDir}${project.path.replace(":", "/")} " freeCompilerArgs += [ "-P", "plugin:androidx.compose.compiler.plugins.kotlin :metricsDestination= ${outPath}", "-P", "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination= ${outPath}" ] } } } } https://github.com/androidx/androidx/blob/403b6d0032c289ed6e65a1c75f137c07aedd220f/compose/compiler/design/compiler-metrics.md TopLevelの build.gradle

Slide 12

Slide 12 text

2.Stableか調べる
 
 1. モジュールごとのSummary(*-module.json) 2. Composable関数の情報(*-composables.txt) 3. Composable関数の引数に与えるクラスの情報(*-classes.txt)

Slide 13

Slide 13 text

2.Stableか調べる
 
 { "skippableComposables": 81, "restartableComposables": 86, "readonlyComposables": 0, "totalComposables": 86, "restartGroups": 86, "totalGroups": 129, "staticArguments": 129, "certainArguments": 52, "knownStableArguments": 961, "knownUnstableArguments": 20, "unknownStableArguments": 10, "totalArguments": 991, "markedStableClasses": 0, "inferredStableClasses": 16, "inferredUnstableClasses": 4, "inferredUncertainClasses": 1, "effectivelyStableClasses": 16, "totalClasses": 21, "memoizedLambdas": 68, "singletonLambdas": 32, "singletonComposableLambdas": 3, "composableLambdas": 12, "totalLambdas": 71 } { "skippableComposables": 81, "restartableComposables": 86, "totalComposables": 86 } *-module.json 5つはSkip可能でない

Slide 14

Slide 14 text

2.Stableか調べる
 
 *-composables.txt
 
 restartable fun ListItem( stable modifier: Modifier unstable uiState: ListItemUiState ) ● 引数が全てstable ● skippableである restartable skippable fun Label( stable drawableRes: Int stable colorRes: Int stable contentDescription: String ) ● 引数にunstableがある ● skippableでない

Slide 15

Slide 15 text

2.Stableか調べる
 
 *-classes.txt
 unstable class ListItemUiState{ stable val iconPath: String? stable val userName: String unstable val labels: List stable val isDraft: Boolean stable val onClick: Function0 = Unstable } ● unstableな値があるとクラスも Unstableになる ● Lambdaはstable

Slide 16

Slide 16 text

2.Stableにする
 
 UnstableをStableにしてskippableにするにはどうしたら良いかの前に 1. Composable関数がskippableで無いのは、必ずしも悪い事では無いと記述 がある https://github.com/androidx/androidx/blob/403b6d0032c289ed6e65a1c75f137c07aedd220f/compose /compiler/design/compiler-metrics.md 2. 公式ドキュメントのサンプルにも普通にunstableを引数に取っている Rettyでは実装が難しく無い限り基本的に StableにしてSkippableにする

Slide 17

Slide 17 text

2.Stableにする
 
 @StableMarker 1. @Immutable ○ publicな値は変更されない ○ MutableStateを通しても値が変更されない 2. @Stable ○ publicな値は変更されない ○ MutableStateを通して値が変更される https://developer.android.com/reference/kotlin/androidx/compose/runtime/StableMarker

Slide 18

Slide 18 text

2.Stableにする
 
 ListをStableにする @Immutable public class ImmutableList(list: List) : List by list.toList()

Slide 19

Slide 19 text

Rettyで使用している設計例


Slide 20

Slide 20 text

2.Rettyで使用している設計例
 Root @Immutable public data class NamesScreenUiState( public val names: ImmutableList , ) @Immutable public data class NameUiState( public val name: String, public val onClick: ComposeEventWrapper <() -> Unit>, } Listの要素 比較時に必ずtrue になるラッパー

Slide 21

Slide 21 text

2.Rettyで使用している設計例
 @Composable public fun NamesScreen ( modifier: Modifier , uiState: NamesScreenUiState , ) { Column(modifier = modifier.fillMaxWidth()) { LazyColumn(modifier = Modifier.fillMaxWidth()) { items(uiState.names) { item -> Name( modifier = Modifier.fillMaxWidth(), uiState = item ) } } } } @Composable public fun Name( modifier: Modifier, uiState: NameUiState, ) { Column( modifier = modifier.clickable { uiState.onClick.event() } ) { Text(text = uiState.name) } }

Slide 22

Slide 22 text

Restart不可能なComposable


Slide 23

Slide 23 text

2.Restart不可能なComposable
 
 
 { "skippableComposables": 81, "restartableComposables": 86, "readonlyComposables": 0, "totalComposables": 86, "restartGroups": 86, "totalGroups": 129, "staticArguments": 129, "certainArguments": 52, "knownStableArguments": 961, "knownUnstableArguments": 20, "unknownStableArguments": 10, "totalArguments": 991, "markedStableClasses": 0, "inferredStableClasses": 16, "inferredUnstableClasses": 4, "inferredUncertainClasses": 1, "effectivelyStableClasses": 16, "totalClasses": 21, "memoizedLambdas": 68, "singletonLambdas": 32, "singletonComposableLambdas": 3, "composableLambdas": 12, "totalLambdas": 71 } { "skippableComposables": 81, "restartableComposables": 86, "totalComposables": 86 } *-module.json

Slide 24

Slide 24 text

2.@NonRestartableComposable
 @Composable public fun Restartable(count: Int) { SideEffect { Log.d("Restartable", "SideEffect") } Text(text = "Restartable: ButtonCount=$count") } @Composable @NonRestartableComposable public fun NonRestartable(count: Int) { SideEffect { Log.d("NonRestartable", "SideEffect") } Text(text = "NonRestartable: ButtonCount=$count") } 1 2 2

Slide 25

Slide 25 text

2.@NonRestartableComposable
 var nonRestartable by remember { mutableStateOf(0) } var restartable by remember { mutableStateOf(0) } Column { Button(onClick = { restartable++ }) { Text(text = "Restartable") } Restartable(restartable) Button(onClick = { nonRestartable++ }) { Text(text = "NonRestartable") } NonRestartable(nonRestartable) } Restartable D SideEffect NonRestartable D SideEffect ログ出力

Slide 26

Slide 26 text

2.@NonRestartableComposable
 Compose 1.1 での使用箇所

Slide 27

Slide 27 text

2.@NonRestartableComposable
 Compose 1.2.0 (beta2) での使用箇所

Slide 28

Slide 28 text

2.@NonRestartableComposable
 リスタート不可能にすると良い時 ● 再コンポジションの「ルート」になることがないと 考えられる場合 ● つまり、Composable関数が直接変数を読み込まない場 合 全て別のComposableに渡しているので、 Card側は再起動可能にする必要はない

Slide 29

Slide 29 text

Layoutフェーズだけを実行する


Slide 30

Slide 30 text

3.Layoutフェーズだけを実行する
 
 スクロールしても画像の位置は 変わっていないように見える CollapsingLayout

Slide 31

Slide 31 text

3.Layoutフェーズだけを実行する
 
 @Composable public fun CollapsingLayout ( modifier: Modifier = Modifier , scrollOffset: Int , content: @Composable () -> Unit ) { Layout( modifier = modifier, content = content ) { measurables, constraints -> TODO("scrollOffsetを使って計算") } } 1 2 Compositionの回数 ※ComposeRuntime1.2から使える LayoutInspectorの機能

Slide 32

Slide 32 text

3.Layoutフェーズだけを実行する
 
 @Composable public fun CollapsingLayout ( modifier: Modifier = Modifier , scrollOffsetProvider: () -> Int, content: @Composable () -> Unit ) { Layout( modifier = modifier, content = content ) { measurables, constraints -> val scrollOffset = scrollOffsetProvider() TODO("scrollOffsetを使って計算") } } 1 2 ※ComposeRuntime1.2から使える LayoutInspectorの機能 Compositionの回数

Slide 33

Slide 33 text

3.Layoutフェーズだけを実行する
 
 公式の例では、offsetが、Lambda版と非Lambda版がある。 ● 固定サイズ -> 非Lambda版 ● アニメーションする -> Lambda版

Slide 34

Slide 34 text

最後に: Composeで作られている部分