Slide 1

Slide 1 text

Jetpack Compose メカニズム

Slide 2

Slide 2 text

Jaewoong Eum Senior Developer Advocate @ RevenueCat skydoves @github_skydoves

Slide 3

Slide 3 text

年間 40,000,000+ ライブラリダウンロード 2,000,000,000+ エンドユーザーデバイス github.com/skydoves

Slide 4

Slide 4 text

Compose の歴史

Slide 5

Slide 5 text

Composable 関数 @Composable fun Greeting(name: String) { Text("Hello, $name") }

Slide 6

Slide 6 text

Compose の構造 コード 変換 レンダリング Layout Tree インメモリ 表現

Slide 7

Slide 7 text

Composable 関数 @Composable fun Greeting(name: String) { Text("Hello, $name") }

Slide 8

Slide 8 text

Composable 関数 呼び出しコンテキスト (calling context) ✅ ❌ @Composable fun Place(name: String) { Person(name) } fun place(name: String) { Person(name) } @Composable fun Person(name: String) { // composable コード } @Composable fun Person(name: String) { // composable コード }

Slide 9

Slide 9 text

Composable 関数 Composable 関数 Kotlin Coroutines @Composable fun Place(name: String) { Person(name) } suspend fun fetchPlace(name: String): Place { getPlaceFromDB(name = name) } @Composable fun Person(name: String) { // composable コード } setContent { Place(name = "skydoves") } suspend fun getPlaceFromDB(name: String): Place { // 処理... } coroutineScope.launch { fetchPlace(name = "skydoves") }

Slide 10

Slide 10 text

Composable 関数 Coroutines suspend fun fetchPlace(name: String): Place { // 処理... } コンパイル fun fetchPlace( name: String, callback: Continuation ) { // 処理... } ● Kotlin は suspending function と coroutine による non-blocking suspension をサポートします。 ● Suspend function は、別の suspending context からのみ呼び出せます。 ● compiler は suspend function を、暗黙的な Continuation

Slide 11

Slide 11 text

Composable 関数 fun Greeting(name: String, $composer: Composer?, $changed: Int) { $composer = $composer.startRestartGroup() @Composable fun Greeting(name: String) { Text("Hello, $name") } Compose Compiler .. real function body (skipped) .. $composer.endRestartGroup()?.updateScope { nc, _ -> Geeting(name, nc, updateChangedFlags($changed) or 0b1) } }

Slide 12

Slide 12 text

Composable 関数 ● Compose Compiler は composable function の intermediate representation IR を変換します。 ● Compose はすべての composable function の末尾に新しい parameter、$composer を追加します。 ● $composer は composable call 間で渡される runtime context で、compiler が生成したコードから Compose Runtime と連携できるようにします。

Slide 13

Slide 13 text

Composable 関数 fun NamePlate( name: String, lastname: String, $composer: Composer<*> ) { ... Column(modifier = Modifier.padding(16.dp), $composer) { Text( text = name, $composer Compose Compiler ) Text( text = lastname, $composer ) } ... } @Composable fun NamePlate(name: String, lastname: String) { Column(modifier = Modifier.padding(16.dp)) { Text(text = name) Text(text = lastname) } }

Slide 14

Slide 14 text

Composable 関数 ● Composable function は通常の関数と異なり、recomposition によって再実行されることがあります。 ● Compose Compiler は composable function を変換し、recomposition 中に効率よく restart や skip できるよ うにします。

Slide 15

Slide 15 text

Composable 関数 fun Greeting(name: String, $composer: Composer?, $changed: Int) { $composer = $composer.startRestartGroup() ← 開始 @Composable fun Greeting(name: String) { Text("Hello, $name") } Compose Compiler .. skipped .. $composer.endRestartGroup()?.updateScope { nc, _ -> ← 終了 Geeting(name, nc, updateChangedFlags($changed) or 0b1) } }

Slide 16

Slide 16 text

Composable 関数 常に同じ ● 同じ入力 parameter で Composable function を複数回実行しても、常に同じ UI tree を生成する必要がありま す。 ● Compose Runtime は recomposition などの処理で、この前提(Idempotent)に依存しています。 ● Compose Runtime は Composable function が idempotent であるとみなし、同じ入力なら再実行しません。

Slide 17

Slide 17 text

Compose Compiler @Composable fun Greeting(name: String) { Text("Hello, $name") } ≠ fun Greeting(name: String) { Text("Hello, $name") }

Slide 18

Slide 18 text

Compose Compiler Hilt & Dagger class ExampleActivity extends Activity { @BindView(R.id.title) TextView title; @BindView(R.id.subtitle) TextView subtitle; @BindView(R.id.footer) TextView footer; @Override public void onCreate(Bundle savedIns) { super.onCreate(savedIns); setContentView(R.layout.simple_activity); ButterKnife.bind(this); // TODO フィールドを使う ... } } @AndroidEntryPoint class MyActivity : MyBaseActivity() { // SingletonComponent または ActivityComponent の bindings @Inject lateinit var bar: Bar override fun onCreate(savedInstanceState: Bundle?) { // Injection は super.onCreate() で行われます。 super.onCreate() // bar を使って何かする ... } }

Slide 19

Slide 19 text

Compose Compiler ButterKnife, Hilt, Dagger, Moshi, etc Jetpack Compose

Slide 20

Slide 20 text

Compose Compiler Compose Compiler は Kotlin compiler plugin として Jetpack Compose の重要な役割を担います。 KAPT や KSP と異なり、Compose Compiler は Kotlin compiler の内部で動作するため、コンパイル中に composable function を解析し、変換できます。

Slide 21

Slide 21 text

Compose Compiler @Composable fun Greeting(name: String) { Text("Hello, $name") } Compose Compiler fun Greeting(name: String, $composer: Composer?, $changed: Int) { $composer = $composer.startRestartGroup() var $dirty = $changed if ($changed and 0b0110 == 0) { $dirty = $dirty or (if ($composer.changed(name)) 0b0100 else 0b0010) } if ($dirty and 0b0011 != 0b0010 || !$composer.skipping) { Text("Hello, $name", $composer, 0) } else { $composer.skipToGroupEnd() } $composer.endRestartGroup()?.updateScope { nc, _ -> Geeting(name, nc, updateChangedFlags($changed) or 0b1) } }

Slide 22

Slide 22 text

Compose Compiler @Composable fun Greeting(name: String) { Text("Hello, $name") } Compose Compiler fun Greeting(name: String, $composer: Composer?, $changed: Int) { $composer = $composer.startRestartGroup() var $dirty = $changed if ($changed and 0b0110 == 0) { $dirty = $dirty or (if ($composer.changed(name)) 0b0100 else 0b0010) } if ($dirty and 0b0011 != 0b0010 || !$composer.skipping) { Text("Hello, $name", $composer, 0) } else { $composer.skipToGroupEnd() } $composer.endRestartGroup()?.updateScope { nc, _ -> Geeting(name, nc, updateChangedFlags($changed) or 0b1) } }

Slide 23

Slide 23 text

Compose Compiler @Composable fun Greeting(name: String) { Text("Hello, $name") } Compose Compiler fun Greeting(name: String, $composer: Composer?, $changed: Int) { $composer = $composer.startRestartGroup() var $dirty = $changed if ($changed and 0b0110 == 0) { $dirty = $dirty or (if ($composer.changed(name)) 0b0100 else 0b0010) } if ($dirty and 0b0011 != 0b0010 || !$composer.skipping) { Text("Hello, $name", $composer, 0) } else { $composer.skipToGroupEnd() } $composer.endRestartGroup()?.updateScope { nc, _ -> Geeting(name, nc, updateChangedFlags($changed) or 0b1) } }

Slide 24

Slide 24 text

Compose Compiler fun Greeting(name: String, $composer: Composer?, $changed: Int) { $composer = $composer.startRestartGroup() var $dirty = $changed if ($changed and 0b0110 == 0) { $dirty = $dirty or (if ($composer.changed(name)) 0b0100 else 0b0010) @Composable } fun Greeting(name: String) { if ($dirty and 0b0011 != 0b0010 || !$composer.skipping) { Text("Hello, $name") Text("Hello, $name", $composer, 0) } Compose Compiler } else { $composer.skipToGroupEnd() Skip! } $composer.endRestartGroup()?.updateScope { nc, _ -> Geeting(name, nc, updateChangedFlags($changed) or 0b1) } }

Slide 25

Slide 25 text

Compose Compiler @Composable fun Greeting(name: String) { Text("Hello, $name") } Compose Compiler fun Greeting(name: String, $composer: Composer?, $changed: Int) { $composer = $composer.startRestartGroup() var $dirty = $changed if ($changed and 0b0110 == 0) { $dirty = $dirty or (if ($composer.changed(name)) 0b0100 else 0b0010) } if ($dirty and 0b0011 != 0b0010 || !$composer.skipping) { Text("Hello, $name", $composer, 0) } else { $composer.skipToGroupEnd() } $composer.endRestartGroup()?.updateScope { nc, _ -> Geeting(name, nc, updateChangedFlags($changed) or 0b1) } }

Slide 26

Slide 26 text

Compose Runtime 入力値の変更 recomposition のたびに、runtime は各 argument を前回の composition で保持した値と比較しま す。1つでも異なれば、 composable は再実行されます。どちらか一方だけでも十分です。 fun Greeting(name: String, $composer: Composer?, $changed: Int) { .. var $dirty = $changed equals() でチェック if ($changed and 0b0110 == 0) { $dirty = $dirty or (if ($composer.changed(name)) 0b0100 else 0b0010) } if ($dirty and 0b0011 != 0b0010 || !$composer.skipping) { Text("Hello, $name", $composer, 0) } Recomposition! .. }

Slide 27

Slide 27 text

Compose Runtime State の変更を監視 Jetpack Compose は、Compose runtime library が提供する State API を使って state の変更を監 視し、recomposition をトリガーする仕組みを提供します。 State を読み取った composable は、その State に依存しているものとして記録されます。 State が変わ ると、runtime はその composable を再実行対象としてマークします。 var bird by remember { mutableStateOf(..) }

Slide 28

Slide 28 text

Compose Runtime @Composable fun BirdProfile() { var bird = Dove() } vs. @Composable fun BirdProfile() { var bird = remember { Dove() } } vs. @Composable fun BirdProfile() { var bird = mutableStateOf(Dove()) } vs. @Composable fun BirdProfile() { var bird = remember { mutableStateOf(Dove()) } }

Slide 29

Slide 29 text

Compose Runtime メモリ (memory) @Composable fun BirdProfile() { var bird = Dove() } #Run 1 Dove@2a139a55 Recompose @Composable fun BirdProfile() { var bird = Dove() } #Run 1 Dove@2a139a55 #Run 2 Dove@15db9742 Recompose @Composable fun BirdProfile() { var bird = Dove() } #Run 1 Dove@2a139a55 #Run 2 Dove@15db9742 #Run 3 Dove@6d06d69c

Slide 30

Slide 30 text

Compose Runtime メモリ (memory) @Composable fun BirdProfile() { var bird = remember { Dove() } } #Run 1 Dove@2a139a55 Recompose @Composable fun BirdProfile() { var bird = remember { Dove() } } #Run 1 Dove@2a139a55 Recompose @Composable fun BirdProfile() { var bird = remember { Dove() } } #Run 1 Dove@2a139a55

Slide 31

Slide 31 text

Compose Runtime remember API ● 値を composition の SlotTable に保存します。 ● key が変わるまで、 recomposition をまたいで保存した 値を再利用します。 SlotTable システム ● ● composition の構造と runtime state を保存します。 Composer が recomposition をまたいで値を見つけ、 再利用できるようにします。

Slide 32

Slide 32 text

Compose Runtime @Composable fun Counter() { var count by remember { mutableStateOf(0) } if (count > 0) { Icon(Icons.Default.Star, null) } Text("Count: $count") }

Slide 33

Slide 33 text

Compose Runtime @Composable fun Counter() { var count by remember { mutableStateOf(0) } if (count > 0) { Icon(Icons.Default.Star, null) } Text("Count: $count") }

Slide 34

Slide 34 text

Compose Runtime @Composable fun Counter() { var count by remember { mutableStateOf(0) } if (count > 0) { Skip! Icon(Icons.Default.Star, null) } Text("Count: $count") }

Slide 35

Slide 35 text

Compose Runtime @Composable fun Counter() { var count by remember { mutableStateOf(1) } if (count > 0) { Icon(Icons.Default.Star, null) } Text("Count: $count") }

Slide 36

Slide 36 text

Compose Runtime Gap Buffer ● SlotTable は group と slot を効率よく保存するために Gap Buffer を 使います。 ● gap を編集位置の近くへ移動することで、 composition 中の挿入や削 除を低コストにします。テキストエディタと似ています。 @Composable fun Counter() { var count by remember { mutableStateOf(1) } if (count > 0) { Icon(Icons.Default.Star, null) } Text("Count: $count") } スペースを挿 入!

Slide 37

Slide 37 text

Compose Runtime @Composable fun Counter() { var count by remember { mutableStateOf(1) } if (count > 0) { Icon(Icons.Default.Star, null) } Text("Count: $count") }

Slide 38

Slide 38 text

Compose Runtime @Composable fun Counter() { var count by remember { mutableStateOf(1) } if (count > 0) { Icon(Icons.Default.Star, null) } Text("Count: $count") }

Slide 39

Slide 39 text

Compose Runtime 位置ベースのメモ化 ● 呼び出し位置が identity の一部になります。 ● 同じ呼び出しが同じ位置に現れると、値が再利用されます。 ● 各呼び出し位置は、それぞれ独立した state を保持できます。 SlotTable は composition の runtime state と構造をキャッシュし、 Composer が recomposition 中にそれらを再利用できるようにします。

Slide 40

Slide 40 text

Compose Runtime @Composable fun Counter() { var count = 0 Text("Count: $count") Button(onClick= { counter++ }) {..} } vs. @Composable fun Counter() { var count = remember { 0 } Text("Count: $count") Button(onClick= { counter++ }) {..} } 3 clicks → var count = 3 → recomposition → var count = 0

Slide 41

Slide 41 text

Compose Runtime @Composable fun Counter() { var count = 0 Text("Count: $count") Button(onClick= { counter++ }) {..} } 3 clicks → var count = 3 → recomposition → var count = 0 vs. @Composable fun Counter() { var count = remember { 0 } Text("Count: $count") Button(onClick= { counter++ }) {..} } 3 clicks → var count = 3 → recomposition → var count = 3 runtime の SlotTable に値を保存します。

Slide 42

Slide 42 text

Compose Runtime UI は一度も更新されていません @Composable fun Counter() { var count = 0 Text("Count: $count") Button(onClick= { counter++ }) {..} } 3 clicks → var count = 3 → recomposition → var count = 0 vs. @Composable fun Counter() { var count = remember { 0 } Text("Count: $count") Button(onClick= { counter++ }) {..} } 3 clicks → var count = 3 → recomposition → var count = 3

Slide 43

Slide 43 text

Compose Runtime State とは? ● Compose 向けに設計された observable な value holder。 ● LiveData や StateFlow と異なり、Compose の State では明示的に subscribe/unsubscribe する必要がありません。 LiveData observe(owner) { ... } ● observe() や collect() の呼び出しはありません。 composable が state.value を読むと、Compose system がその read を自動的に 追跡します。値が変わると、その値を読んだ composable に対して Compose が recomposition をスケジュールします。 StateFlow collect { ... } State // 何もしない。ただ読むだけ。 その意味では、値を読むこと自体が実質的な subscription です。

Slide 44

Slide 44 text

Compose Runtime State & Snapshot システム ● mutableStateOf() が返すのは単なる箱ではありません。 Compose の Snapshot system に支えられた state です。 ● Snapshot system は Compose に一貫した state の view を提供し、 read の監視と時間経過に伴う変更の管理を可能にします。 ● その state が変わると、 Compose はどの処理を再実行する必要があ るか判断できます。 Snapshot は Compose state の下にある consistency layer と考えられま す。

Slide 45

Slide 45 text

Compose Runtime ● getValue() は一見すると普通の getter です。明示的な subscription はありませんが、 read は痕跡を残 し ます。 ● Compose は、どの scope がどの state を読んだかを覚えています。その state が変わると、 対応する scope が invalidated され、recomposition できるようになります。 // 書いたコード var count by mutableStateOf(0) Text("Count: $count") // 概念的には val count = mutableStateOf(0) Text("Count: " + count.getValue(), $composer, 0) // getValue() はこれだけの getter get() = next.readable(this).value

Slide 46

Slide 46 text

Compose Runtime ● write はすぐに recompose しません。依存する scope を invalidate し、recomposition をスケジュールします。 ● recomposition 前の複数の write はまとめられることがあり、 3回の write が必ず3回の recomposition になるとは限りません。 state をどこで読むかによって、その state が変わったときに何が recomposition されるかが決まります。つまり state read が、どの recomposition scope がその state に依存するかを決めます。

Slide 47

Slide 47 text

Compose Runtime @Composable fun BirdProfile() { var bird by mutableStateOf(Dove()) } vs. @Composable fun BirdProfile() { var bird by remember { mutableStateOf(Dove()) } }

Slide 48

Slide 48 text

Compose Runtime @Composable fun Counter() { var count by mutableStateOf(0) Text("Count: $count") Button(onClick= { counter++ }) {..} } vs. @Composable fun Counter() { var count by remember { mutableStateOf(0) } Text("Count: $count") Button(onClick= { counter++ }) {..} } 3 clicks → var count = State(3 → recomposition → var count = State(0

Slide 49

Slide 49 text

Compose Runtime @Composable fun Counter() { var count by mutableStateOf(0) Text("Count: $count") Button(onClick= { counter++ }) {..} } 3 clicks → var count = State(3 → recomposition → var count = State(0 UI は更新されていません ! vs. @Composable fun Counter() { var count by remember { mutableStateOf(0) } Text("Count: $count") Button(onClick= { counter++ }) {..} }

Slide 50

Slide 50 text

Compose Runtime @Composable fun Counter() { var count by mutableStateOf(0) Text("Count: $count") Button(onClick= { counter++ }) {..} } vs. 3 clicks → var count = State(3 → recomposition → var count = State(0 UI が更新されました ! @Composable fun Counter() { var count by remember { mutableStateOf(0) } Text("Count: $count") Button(onClick= { counter++ }) {..} } 3 clicks → var count = State(3 → recomposition → var count = State(3

Slide 51

Slide 51 text

Compose の構造

Slide 52

Slide 52 text

Compose の構造 ? ?

Slide 53

Slide 53 text

Compose UI ● Column、Row、Text、Image はすべて同じ基盤構造、つまり LayoutNode hierarchy を構成します。 ● Layout、drawing、input、focus、accessibility は、この構造の上に成り立っています。 ● Button の背後に android.view.View があるわけではありません。その下には Compose が所有する node hierarchy があります。

Slide 54

Slide 54 text

Compose UI ● SlotTable は UI そのものではありません。何が、どの順番で呼ばれたかを記録する flat な構造です。 ● 実際に measure と draw の対象になるのは tree です。親子関係は node 側に保持されます。

Slide 55

Slide 55 text

Compose UI ● Applier。node を insert、remove、move する。interface のほぼすべてはこれです。 ● tree が LayoutNode で構成されていると知っているのは、 Compose の中でもこの部分だけです。 ● runtime 自体は tree に触れません。だからこそ同じ runtime を Android、desktop、web で使えます。

Slide 56

Slide 56 text

Compose UI // compose-runtime interface Applier { fun insertBottomUp(index: Int, instance: N) fun remove(index: Int, count: Int) fun move(from: Int, to: Int, count: Int) ... } // compose-ui internal class UiApplier(root: LayoutNode): AbstractApplier(root) // compose-ui, androidMain internal class AndroidComposeView(...) : ViewGroup(context), Owner, ...

Slide 57

Slide 57 text

Compose UI // compose-runtime interface Applier { fun insertBottomUp(index: Int, instance: N) fun remove(index: Int, count: Int) fun move(from: Int, to: Int, count: Int) ... } // compose-ui internal class UiApplier(root: LayoutNode): AbstractApplier(root) // compose-ui, androidMain internal class AndroidComposeView(...) : ViewGroup(context), Owner, ...

Slide 58

Slide 58 text

Compose UI ● Text、Image、Icon、Row、Column、Box など、すべての UI component は最終的に `Layout` composable function を呼び出します。 ● LayoutNode を作るのは Layout だけです。それ以外は作りません。 ● 違いを生むのは 2つの argument、どう measure するか、そしてどの modifiers を持つかです。 Layout(finalModifier, EmptyMeasurePolicy)

Slide 59

Slide 59 text

Compose UI ● Text は、何も measure しない policy を持つ Layout です。 ● BasicText は最終的に 1行の Layout 呼び出しに到達し、渡される measure policy 自体は何も measure しません。 ● Text は compose-material3、BasicText は compose-foundation、Layout は compose-ui です。 // foundation: BasicText の最後の行 Layout(modifier then TextStringSimpleElement(text, style, ...), EmptyMeasurePolicy) // EmptyMeasurePolicy 自体は何も measure しない layout(constraints.maxWidth, constraints.maxHeight, placementBlock)

Slide 60

Slide 60 text

Compose UI // AndroidComposeView: ViewGroup, Owner internal class AndroidComposeView(..): ViewGroup(context), Owner { override val root = LayoutNode() override fun dispatchDraw(canvas: android.graphics.Canvas) { measureAndLayout() canvasHolder.drawInto(canvas) { root.draw(canvas = this, graphicsLayer = null) } } .. } // Composition はここに紐づく Composition(UiApplier(composeView.root), parent)

Slide 61

Slide 61 text

Compose Phases コード 変換 レンダリング Layout Tree インメモリ 表現

Slide 62

Slide 62 text

Compose Performance goo.gle/hero-benchmark

Slide 63

Slide 63 text

Compose Performance ● Composition: 何を表示するか。 Compose が composable function を実行し、UI の表現を構築します。 ● Layout: どこに配置するか。各 node を measure し、layout tree の中で位置を決めます。 ● Drawing: どう描画するか。 UI element が content を Canvas、通常はデバイス画面へ描画します。

Slide 64

Slide 64 text

Compose Performance

Slide 65

Slide 65 text

Compose Performance

Slide 66

Slide 66 text

Compose Performance Smart Recomposition

Slide 67

Slide 67 text

Compose Performance skip recomposition skip

Slide 68

Slide 68 text

Compose Performance Kotlin 2.0.10(Strong Skipping Mode 以前) ● parameter type に unstable が1つでもあると、composable は skip できず、値を比較せずに recomposition さ れます。 ● すべての parameter type が stable なら、Compose は equals() で比較します。1つでも parameter が変わって いれば recomposition されます。

Slide 69

Slide 69 text

Compose Performance Kotlin 2.0.10(Strong Skipping Mode 以前) ● parameter type に unstable が1つでもあると、composable は skip できず、値を比較せずに recomposition さ れます。 ● すべての parameter type が stable なら、Compose は equals() で比較します。1つでも parameter が変わって いれば recomposition されます。 Kotlin 2.0.20(Strong Skipping Mode 以降) ● Unstable parameter は referential equality (===) で比較されます ● Stable parameter は structural equality Object.equals()) で比較されます

Slide 70

Slide 70 text

Compose Performance unstable parameter 値を capture する lambda

Slide 71

Slide 71 text

Compose Performance 多くの場合、Strong Skipping Mode で十分です。 ただし、Strong Skipping Mode はどの type の stability も変更しません。Unstable type は unstable のままです。変わ るのは、skip check のときに runtime が unstable parameter をどう扱うかです。 unstable value が頻繁に新しい instance として作り直される場合、stability は今でも重要です。 restartable fun Avatar( stable modifier: Modifier? = @static Companion stable imageUrl: String? = @static null stable initials: String? = @static null stable shape: Shape? = @dynamic ... unstable badges: List ) Strong Skipping skippable restartable fun Avatar( stable modifier: Modifier? = @static Companion stable imageUrl: String? = @static null stable initials: String? = @static null stable shape: Shape? = @dynamic ... unstable badges: List )

Slide 72

Slide 72 text

Compose Performance @Composable fun UserScreen(users: List) { UserList( users = users.map { user -> UserProfile( name = user.name, avatarUrl = user.avatarUrl ) } ) } 親が recomposition → map() が毎回新しい List を作る → List は unstable → === が失敗 → UserList は skip できない

Slide 73

Slide 73 text

Compose Performance @Composable fun UserScreen(users: List) { val profiles = remember(users) { users.map { user -> UserProfile( name = user.name, avatarUrl = user.avatarUrl ) } } UserList(users = profiles) } 親が recomposition → users は変わっていない → remember() が map 済みの List を再利用 → 同じ List instance → === が成功 → UserList は skip できる

Slide 74

Slide 74 text

Compose Performance data class UiState( val items: List = emptyList() ) class DemoViewModel : ViewModel() { private val _uiState = MutableStateFlow(UiState()) val uiState = _uiState.asStateFlow() fun updateItems() { _uiState.update { state -> state.copy( // 新しい instance を作る items = state.items.map { it.copy() } ) } } }

Slide 75

Slide 75 text

Compose Performance data class UiState( val items: List = emptyList() ) @Composable fun DemoScreen(viewModel: DemoViewModel) { val uiState by viewModel.uiState.collectAsState() class DemoViewModel : ViewModel() { private val _uiState = MutableStateFlow(UiState()) val uiState = _uiState.asStateFlow() fun updateItems() { _uiState.update { state -> state.copy( // 新しい instance を作る items = state.items.map { it.copy() } ) } } } ItemList( items = uiState.items ) }

Slide 76

Slide 76 text

Compose Performance data class UiState( val items: List = emptyList() ) @Composable fun DemoScreen(viewModel: DemoViewModel) { val uiState by viewModel.uiState.collectAsState() class DemoViewModel : ViewModel() { private val _uiState = MutableStateFlow(UiState()) val uiState = _uiState.asStateFlow() } ItemList( items = uiState.items ) } fun updateItems() { _uiState.update { state -> state.copy( // 新しい instance を作る items = state.items.map { it.copy() } ) distinctuntilchanged() → equals(==) check } } val oldState = _state.value if (expectedState != null && oldState != expectedState) return false if (oldState == newState) return true // equals と同じ _state.value = newState

Slide 77

Slide 77 text

Compose Performance class MessageRepository( private val dao: MessageDao unstable ) { fun observeMessages(): Flow> = dao.observeMessages() .map { entities -> // 新しい instances entities.map { it.toMessage() } } }

Slide 78

Slide 78 text

Compose Performance class MessageRepository( private val dao: MessageDao unstable ) { fun observeMessages(): Flow> = dao.observeMessages() .map { entities -> // 新しい instances entities.map { it.toMessage() } } } class MessageViewModel( repository: MessageRepository ) : ViewModel() { val messages = repository.observeMessages() .stateIn( distinctuntilchanged() → equals(==) check viewModelScope, SharingStarted.WhileSubscribed(), emptyList() ) }

Slide 79

Slide 79 text

Compose Performance class MessageRepository( private val dao: MessageDao unstable ) { fun observeMessages(): Flow> = dao.observeMessages() .map { entities -> // 新しい instances entities.map { it.toMessage() } } } class MessageViewModel( repository: MessageRepository ) : ViewModel() { @Composable fun MessageScreen( viewModel: MessageViewModel ) { val messages by viewModel.messages.collectAsState() LazyColumn { items( items = messages, key = { it.id } ) { message -> // これは unstable // 新しい unstable instance → === が失敗 MessageBubble(message = message) } } New instance → Skip check fails → val messages = repository.observeMessages() } .stateIn( distinctuntilchanged() → equals(==) check viewModelScope, SharingStarted.WhileSubscribed(), emptyList() ) } Recomposition

Slide 80

Slide 80 text

Compose Performance data class Message( val id: String, var isRead: Boolean, // mutable val reactions: List // unstable collection }

Slide 81

Slide 81 text

Compose Performance data class Message( val id: String, var isRead: Boolean, // mutable val reactions: List // unstable collection } data class Message( val id: String, val isRead: Boolean, // read-only val reactions: ImmutableList // stable collection }

Slide 82

Slide 82 text

Compose Performance data class Message( val id: String, var isRead: Boolean, // mutable val reactions: List // unstable collection } @Immutable data class Message( val id: String, val isRead: Boolean, // read-only val reactions: List // unstable collection }

Slide 83

Slide 83 text

Compose Performance Now in Android For You Feed Screen UserNewsResource model(Strong Skipping Mode 有効) ● ● unstable: 140 recompositions stable: 30 recompositions Stability configuration file // data model では常に immutable class を使う com.google.samples.apps.nowinandroid.core.model.data.* → Immutable または Stable を使って、これらの model を指定することも できます。

Slide 84

Slide 84 text

Compose Performance Compose Stability Analyzer Plugin github.com/skydoves/compose-stability-analyzer

Slide 85

Slide 85 text

Compose Performance Jetpack Compose Mechanisms Book 1. Compose Compiler 2. Compose Runtime 3. Compose UI 4. Compose Performance 🔗: howcomposeworks.com

Slide 86

Slide 86 text

ありがとうございました。 skydoves @github_skydoves