$30 off During Our Annual Pro Sale. View Details »

Jetpack Composeのパフォーマンスの基本

松田
June 30, 2022

Jetpack Composeのパフォーマンスの基本

以下で話したJetpack Composeのパフォーマンスの基本についてのスライドです
https://andpad.connpass.com/event/249842/

松田

June 30, 2022
Tweet

Other Decks in Technology

Transcript

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


  2. 自己紹介
 Retty株式会社
 アプリチーム所属
 松田
 
 • 2020年入社 3年目
 • やっていること


    ◦ 主にAndroid
 ◦ たまにiOS
 ◦ バックエンドサーバー(Kotlin) 
 アプリエンジニア 特にAndroid / Kotlinバックエンドサーバー に興味があるエンジニア募集中
  3. Rettyとは


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

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

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

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

  8. 目次
 1. Jetpack Composeの動作フェーズ
 2. Composable
 2.1. Compositionをskipする
 2.2. Rettyで使用している設計例


    2.3. Restart不可能なComposable
 3. Layout
 3.1. Layoutフェーズだけを実行する

  9. 2.StableとSkippable
 @Composable public fun MainContent( title: String, color: Color, items:

    List<String>, ) { // ~~~~ } 引数がequalsで比較して同一 であればそのComposable内 のCompositionはスキップされ る ※Composable関数がStable である場合に限る
  10. 2.StableとSkippable
 val mutableItems = mutableListOf<String>() val items: List<String> = mutableItems

    mutableItems.add("1") println(items) // [1] 1. Listは外部から変更可能。 → これがStableではないという事 2. 外部から変更された事をComposeは検知できない。 3. なのでskipされずに毎回Compositionが行われる。
  11. 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
  12. 2.Stableか調べる
 
 1. モジュールごとのSummary(*-module.json) 2. Composable関数の情報(*-composables.txt) 3. Composable関数の引数に与えるクラスの情報(*-classes.txt)

  13. 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可能でない
  14. 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でない
  15. 2.Stableか調べる
 
 *-classes.txt
 unstable class ListItemUiState{ stable val iconPath: String?

    stable val userName: String unstable val labels: List<String> stable val isDraft: Boolean stable val onClick: Function0<Unit> <runtime stability> = Unstable } • unstableな値があるとクラスも Unstableになる • Lambdaはstable
  16. 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にする
  17. 2.Stableにする
 
 @StableMarker 1. @Immutable ◦ publicな値は変更されない ◦ MutableStateを通しても値が変更されない 2.

    @Stable ◦ publicな値は変更されない ◦ MutableStateを通して値が変更される https://developer.android.com/reference/kotlin/androidx/compose/runtime/StableMarker
  18. 2.Stableにする
 
 ListをStableにする @Immutable public class ImmutableList<E>(list: List<E>) : List<E>

    by list.toList()
  19. Rettyで使用している設計例


  20. 2.Rettyで使用している設計例
 Root @Immutable public data class NamesScreenUiState( public val names:

    ImmutableList<NameUiState> , ) @Immutable public data class NameUiState( public val name: String, public val onClick: ComposeEventWrapper <() -> Unit>, } Listの要素 比較時に必ずtrue になるラッパー
  21. 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) } }
  22. Restart不可能なComposable


  23. 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
  24. 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
  25. 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 ログ出力
  26. 2.@NonRestartableComposable
 Compose 1.1 での使用箇所

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

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

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


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

  31. 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の機能
  32. 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の回数
  33. 3.Layoutフェーズだけを実行する
 
 公式の例では、offsetが、Lambda版と非Lambda版がある。 • 固定サイズ -> 非Lambda版 • アニメーションする ->

    Lambda版
  34. 最後に: Composeで作られている部分