Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Jetpack Compose メカニズム

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.
Avatar for Jaewoong Jaewoong
September 07, 2026

Jetpack Compose メカニズム

Nowadays, Jetpack Compose is widely used to build modern Android and multiplatform applications, and it has become much more than just a UI toolkit.

Have you ever wondered how the “magic” behind the `@Composable` function actually works?

In this session, we’ll explore the internal mechanisms of Jetpack Compose, from its three foundational layers: the Compose Compiler, Compose Runtime, and Compose UI. We’ll look at how composable functions are transformed, how state changes trigger recomposition, how UI is measured and laid out, and how these concepts impact performance in real-world applications.

Topics covered:

* Compose Compiler: `@Composable` internals, IR transformation, and the Composer
* Compose Runtime: SlotTable, Snapshot, and Recomposer
* Compose UI: LayoutNode and Compose phases
* Performance: stability, recomposition, and Hot Reload

Avatar for Jaewoong

Jaewoong

September 07, 2026

More Decks by Jaewoong

Other Decks in Programming

Transcript

  1. 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 コード }
  2. 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") }
  3. Composable 関数 Coroutines suspend fun fetchPlace(name: String): Place { //

    処理... } コンパイル fun fetchPlace( name: String, callback: Continuation<Place> ) { // 処理... } • Kotlin は suspending function と coroutine による non-blocking suspension をサポートします。 • Suspend function は、別の suspending context からのみ呼び出せます。 • compiler は suspend function を、暗黙的な Continuation<T を持つ Continuation-Passing Style CPS に変換します。
  4. Composable 関数 fun Greeting(name: String, $composer: Composer?, $changed: Int) {

    $composer = $composer.startRestartGroup(<key>) @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) } }
  5. Composable 関数 • Compose Compiler は composable function の intermediate

    representation IR を変換します。 • Compose はすべての composable function の末尾に新しい parameter、$composer を追加します。 • $composer は composable call 間で渡される runtime context で、compiler が生成したコードから Compose Runtime と連携できるようにします。
  6. 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) } }
  7. Composable 関数 • Composable function は通常の関数と異なり、recomposition によって再実行されることがあります。 • Compose Compiler

    は composable function を変換し、recomposition 中に効率よく restart や skip できるよ うにします。
  8. Composable 関数 fun Greeting(name: String, $composer: Composer?, $changed: Int) {

    $composer = $composer.startRestartGroup(<key>) ← 開始 @Composable fun Greeting(name: String) { Text("Hello, $name") } Compose Compiler .. skipped .. $composer.endRestartGroup()?.updateScope { nc, _ -> ← 終了 Geeting(name, nc, updateChangedFlags($changed) or 0b1) } }
  9. Composable 関数 常に同じ • 同じ入力 parameter で Composable function を複数回実行しても、常に同じ

    UI tree を生成する必要がありま す。 • Compose Runtime は recomposition などの処理で、この前提(Idempotent)に依存しています。 • Compose Runtime は Composable function が idempotent であるとみなし、同じ入力なら再実行しません。
  10. Compose Compiler @Composable fun Greeting(name: String) { Text("Hello, $name") }

    ≠ fun Greeting(name: String) { Text("Hello, $name") }
  11. 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 を使って何かする ... } }
  12. Compose Compiler Compose Compiler は Kotlin compiler plugin として Jetpack

    Compose の重要な役割を担います。 KAPT や KSP と異なり、Compose Compiler は Kotlin compiler の内部で動作するため、コンパイル中に composable function を解析し、変換できます。
  13. Compose Compiler @Composable fun Greeting(name: String) { Text("Hello, $name") }

    Compose Compiler fun Greeting(name: String, $composer: Composer?, $changed: Int) { $composer = $composer.startRestartGroup(<key>) 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) } }
  14. Compose Compiler @Composable fun Greeting(name: String) { Text("Hello, $name") }

    Compose Compiler fun Greeting(name: String, $composer: Composer?, $changed: Int) { $composer = $composer.startRestartGroup(<key>) 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) } }
  15. Compose Compiler @Composable fun Greeting(name: String) { Text("Hello, $name") }

    Compose Compiler fun Greeting(name: String, $composer: Composer?, $changed: Int) { $composer = $composer.startRestartGroup(<key>) 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) } }
  16. Compose Compiler fun Greeting(name: String, $composer: Composer?, $changed: Int) {

    $composer = $composer.startRestartGroup(<key>) 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) } }
  17. Compose Compiler @Composable fun Greeting(name: String) { Text("Hello, $name") }

    Compose Compiler fun Greeting(name: String, $composer: Composer?, $changed: Int) { $composer = $composer.startRestartGroup(<key>) 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) } }
  18. 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! .. }
  19. Compose Runtime State の変更を監視 Jetpack Compose は、Compose runtime library が提供する

    State API を使って state の変更を監 視し、recomposition をトリガーする仕組みを提供します。 State を読み取った composable は、その State に依存しているものとして記録されます。 State が変わ ると、runtime はその composable を再実行対象としてマークします。 var bird by remember { mutableStateOf(..) }
  20. 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()) } }
  21. 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
  22. 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
  23. Compose Runtime remember API • 値を composition の SlotTable に保存します。

    • key が変わるまで、 recomposition をまたいで保存した 値を再利用します。 SlotTable システム • • composition の構造と runtime state を保存します。 Composer が recomposition をまたいで値を見つけ、 再利用できるようにします。
  24. Compose Runtime @Composable fun Counter() { var count by remember

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

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

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

    { mutableStateOf(1) } if (count > 0) { Icon(Icons.Default.Star, null) } Text("Count: $count") }
  28. 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") } スペースを挿 入!
  29. Compose Runtime @Composable fun Counter() { var count by remember

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

    { mutableStateOf(1) } if (count > 0) { Icon(Icons.Default.Star, null) } Text("Count: $count") }
  31. Compose Runtime 位置ベースのメモ化 • 呼び出し位置が identity の一部になります。 • 同じ呼び出しが同じ位置に現れると、値が再利用されます。 •

    各呼び出し位置は、それぞれ独立した state を保持できます。 SlotTable は composition の runtime state と構造をキャッシュし、 Composer が recomposition 中にそれらを再利用できるようにします。
  32. 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
  33. 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 に値を保存します。
  34. 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
  35. Compose Runtime State とは? • Compose 向けに設計された observable な value

    holder。 • LiveData や StateFlow と異なり、Compose の State では明示的に subscribe/unsubscribe する必要がありません。 LiveData<T> observe(owner) { ... } • observe() や collect() の呼び出しはありません。 composable が state.value を読むと、Compose system がその read を自動的に 追跡します。値が変わると、その値を読んだ composable に対して Compose が recomposition をスケジュールします。 StateFlow<T> collect { ... } State<T> // 何もしない。ただ読むだけ。 その意味では、値を読むこと自体が実質的な subscription です。
  36. Compose Runtime State & Snapshot システム • mutableStateOf() が返すのは単なる箱ではありません。 Compose

    の Snapshot system に支えられた state です。 • Snapshot system は Compose に一貫した state の view を提供し、 read の監視と時間経過に伴う変更の管理を可能にします。 • その state が変わると、 Compose はどの処理を再実行する必要があ るか判断できます。 Snapshot は Compose state の下にある consistency layer と考えられま す。
  37. 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
  38. Compose Runtime • write はすぐに recompose しません。依存する scope を invalidate

    し、recomposition をスケジュールします。 • recomposition 前の複数の write はまとめられることがあり、 3回の write が必ず3回の recomposition になるとは限りません。 state をどこで読むかによって、その state が変わったときに何が recomposition されるかが決まります。つまり state read が、どの recomposition scope がその state に依存するかを決めます。
  39. Compose Runtime @Composable fun BirdProfile() { var bird by mutableStateOf(Dove())

    } vs. @Composable fun BirdProfile() { var bird by remember { mutableStateOf(Dove()) } }
  40. 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
  41. 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++ }) {..} }
  42. 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
  43. Compose UI • Column、Row、Text、Image はすべて同じ基盤構造、つまり LayoutNode hierarchy を構成します。 • Layout、drawing、input、focus、accessibility

    は、この構造の上に成り立っています。 • Button の背後に android.view.View があるわけではありません。その下には Compose が所有する node hierarchy があります。
  44. Compose UI • SlotTable は UI そのものではありません。何が、どの順番で呼ばれたかを記録する flat な構造です。 •

    実際に measure と draw の対象になるのは tree です。親子関係は node 側に保持されます。
  45. Compose UI • Applier。node を insert、remove、move する。interface のほぼすべてはこれです。 • tree

    が LayoutNode で構成されていると知っているのは、 Compose の中でもこの部分だけです。 • runtime 自体は tree に触れません。だからこそ同じ runtime を Android、desktop、web で使えます。
  46. Compose UI // compose-runtime interface Applier<N> { 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<LayoutNode>(root) // compose-ui, androidMain internal class AndroidComposeView(...) : ViewGroup(context), Owner, ...
  47. Compose UI // compose-runtime interface Applier<N> { 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<LayoutNode>(root) // compose-ui, androidMain internal class AndroidComposeView(...) : ViewGroup(context), Owner, ...
  48. Compose UI • Text、Image、Icon、Row、Column、Box など、すべての UI component は最終的に `Layout` composable

    function を呼び出します。 • LayoutNode を作るのは Layout だけです。それ以外は作りません。 • 違いを生むのは 2つの argument、どう measure するか、そしてどの modifiers を持つかです。 Layout(finalModifier, EmptyMeasurePolicy)
  49. 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)
  50. 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)
  51. Compose Performance • Composition: 何を表示するか。 Compose が composable function を実行し、UI

    の表現を構築します。 • Layout: どこに配置するか。各 node を measure し、layout tree の中で位置を決めます。 • Drawing: どう描画するか。 UI element が content を Canvas、通常はデバイス画面へ描画します。
  52. Compose Performance Kotlin 2.0.10(Strong Skipping Mode 以前) • parameter type

    に unstable が1つでもあると、composable は skip できず、値を比較せずに recomposition さ れます。 • すべての parameter type が stable なら、Compose は equals() で比較します。1つでも parameter が変わって いれば recomposition されます。
  53. 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()) で比較されます
  54. 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<String> ) 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<String> )
  55. Compose Performance @Composable fun UserScreen(users: List<User>) { UserList( users =

    users.map { user -> UserProfile( name = user.name, avatarUrl = user.avatarUrl ) } ) } 親が recomposition → map() が毎回新しい List を作る → List は unstable → === が失敗 → UserList は skip できない
  56. Compose Performance @Composable fun UserScreen(users: List<User>) { 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 できる
  57. Compose Performance data class UiState( val items: List<ItemUi> = 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() } ) } } }
  58. Compose Performance data class UiState( val items: List<ItemUi> = 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 ) }
  59. Compose Performance data class UiState( val items: List<ItemUi> = 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
  60. Compose Performance class MessageRepository( private val dao: MessageDao unstable )

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

    { fun observeMessages(): Flow<List<Message>> = 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() ) }
  62. Compose Performance class MessageRepository( private val dao: MessageDao unstable )

    { fun observeMessages(): Flow<List<Message>> = 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
  63. Compose Performance data class Message( val id: String, var isRead:

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

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

    Boolean, // mutable val reactions: List<String> // unstable collection } @Immutable data class Message( val id: String, val isRead: Boolean, // read-only val reactions: List<String> // unstable collection }
  66. 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 を指定することも できます。
  67. Compose Performance Jetpack Compose Mechanisms Book 1. Compose Compiler 2.

    Compose Runtime 3. Compose UI 4. Compose Performance 🔗: howcomposeworks.com