Save 37% off PRO during our Black Friday Sale! »

Hitchhiker's Guide to Compose

2fff9b69a69973e14026624f8c8a9672?s=47 Jossi Wolf
October 20, 2021

Hitchhiker's Guide to Compose

Slides from Amanda Hinchman's and Jossi Wolf's talk "A Hitchhiker's Guide to Compose" given at Droidcon Berlin 2021. Keep your eye out for the updated version after Droidcon London 2021 :)

2fff9b69a69973e14026624f8c8a9672?s=128

Jossi Wolf

October 20, 2021
Tweet

Transcript

  1. Hitchhiker’s Guide to Compose Snapshots, Compiler Plugins and Appliers @mvndy_hd

    @jossiwolf #hg2compose
  2. #hg2compose TextView(text = "") FrameLayout FrameLayout @mvndy_hd @jossiwolf

  3. #hg2compose setText("Bob") TextView(text = "") FrameLayout FrameLayout @mvndy_hd @jossiwolf

  4. #hg2compose FrameLayout setText("Bob") FrameLayout TextView(text = "Bob") @mvndy_hd @jossiwolf

  5. #hg2compose FrameLayout FrameLayout TextView(text = "Bob") getDisplayList() draw() @mvndy_hd @jossiwolf

  6. #hg2compose FrameLayout FrameLayout TextView(text = "Bob") getDisplayList() getDisplayList() @mvndy_hd @jossiwolf

  7. #hg2compose FrameLayout FrameLayout TextView(text = "Bob") getDisplayList() getDisplayList() getDisplayList() @mvndy_hd

    @jossiwolf
  8. #hg2compose FrameLayout FrameLayout TextView(text = "Bob") draw() Bob @mvndy_hd @jossiwolf

  9. #hg2compose Text(text = name) Layout Layout name by remember {

    mutableStateOf("") } @mvndy_hd @jossiwolf
  10. #hg2compose Text(text = name) Layout Layout name by remember {

    mutableStateOf("Bob") } @mvndy_hd @jossiwolf
  11. #hg2compose Text(text = name) Layout Layout name by remember {

    mutableStateOf("Bob") } @mvndy_hd @jossiwolf
  12. #hg2compose Text(text = name) Layout Layout name by remember {

    mutableStateOf("Bob") } Bob @mvndy_hd @jossiwolf
  13. "Jetpack Compose is Android's modern toolkit for building native UI."

    #hg2compose - developer.android.com @mvndy_hd @jossiwolf
  14. Compiler Runtime UI #hg2compose != @mvndy_hd @jossiwolf

  15. A Composable is a restartable function that emits nodes into

    a tree. #hg2compose Glossary @mvndy_hd @jossiwolf
  16. Recomposition is the process of re-executing a Composable function to

    produce an updated slot table #hg2compose Glossary @mvndy_hd @jossiwolf
  17. The Composition holds the Slot Table, invalidations and a Composer.

    #hg2compose Glossary @mvndy_hd @jossiwolf
  18. The Slot Table is used to store the current state

    of the Composition. #hg2compose Glossary @mvndy_hd @jossiwolf
  19. A Composer connects Composables to Runtime, keeps track of groups

    that are used to build the Slot Table and positions it. #hg2compose Glossary @mvndy_hd @jossiwolf
  20. Recomposers manage the Snapshot and Compositions #hg2compose Glossary @mvndy_hd @jossiwolf

  21. @Composable fun HitchhikerApp() { var name by remember { mutableStateOf("Bob")

    } HelloWorld(greeting = name) name = "Chet" } @Composable fun HelloWorld(greeting: String) { Text("Hello, $greeting!") } #hg2compose @mvndy_hd @jossiwolf
  22. Snapshots 📸 #hg2compose @mvndy_hd @jossiwolf

  23. "A Snapshot is a lot like a save point in

    a video game: it represents the state of your entire program at a single point in history." #hg2compose https://dev.to/zachklipp/introduction-to-the-compose-snapshot-system-19cn - "Introduction to the Compose Snapshot System" by Zach Klippenstein Snapshots 📸 (and dogs🐕) @mvndy_hd @jossiwolf
  24. #hg2compose https://dev.to/zachklipp/introduction-to-the-compose-snapshot-system-19cn class Dog { var name: MutableState<String> = mutableStateOf(“”)

    } fun main() { val dog = Dog() dog.name.value = "Spot" val snapshot = Snapshot.takeSnapshot() dog.name.value = "Fido" println(dog.name.value) } Snapshots 📸 (and dogs🐕) @mvndy_hd @jossiwolf
  25. #hg2compose https://dev.to/zachklipp/introduction-to-the-compose-snapshot-system-19cn fun main() { val dog = Dog() dog.name.value

    = "Spot" val snapshot = Snapshot.takeSnapshot() dog.name.value = "Fido" println(dog.name.value) // Fido snapshot.enter { println(dog.name.value) / / Spot } println(dog.name.value) // Fido } Snapshots 📸 (and dogs🐕) @mvndy_hd @jossiwolf
  26. #hg2compose Snapshot.takeSnapshot() Dog(name = "Spot") 📸 Snapshots 📸 (and dogs🐕)

    @mvndy_hd @jossiwolf
  27. #hg2compose Snapshot.takeSnapshot() 📸 Dog(name = "Spot") Snapshots 📸 (and dogs🐕)

    @mvndy_hd @jossiwolf
  28. #hg2compose Snapshot.takeSnapshot() 1 2 📸 Dog(name = "Spot") Snapshots 📸

    (and dogs🐕) @mvndy_hd @jossiwolf
  29. #hg2compose Snapshot.takeSnapshot() dog.name.value = "Fido" 📸 1 2 Dog(name =

    "Spot") Dog(name = "Fido") Snapshots 📸 (and dogs🐕) @mvndy_hd @jossiwolf
  30. #hg2compose Snapshot.takeSnapshot() dog.name.value = "Fido" 📸 1 2 Dog(name =

    "Spot") Dog(name = "Fido") snapshot.enter { . . . } Snapshots 📸 (and dogs🐕) @mvndy_hd @jossiwolf
  31. #hg2compose Snapshot.takeSnapshot() dog.name.value = "Fido" 📸 1 2 Dog(name =

    "Spot") Dog(name = "Fido") snapshot.enter { dog.name.value = "Taco" } ❌ Snapshots 📸 (and dogs🐕) @mvndy_hd @jossiwolf
  32. #hg2compose Snapshot.takeSnapshot() dog.name.value = "Fido" 📸 1 2 Dog(name =

    "Spot") Dog(name = "Fido") snapshot.enter { dog.name.value = "Taco" } ❌ Snapshots 📸 (and dogs🐕) @mvndy_hd @jossiwolf
  33. #hg2compose Snapshot.takeMutableSnapshot() dog.name.value = "Fido" 📸 1 2 Dog(name =

    "Spot") Dog(name = "Fido") snapshot.enter { dog.name.value = "Taco" } ❌ Snapshots 📸 (and dogs🐕) @mvndy_hd @jossiwolf
  34. #hg2compose @mvndy @jossiwolf Snapshots 📸 (and dogs🐕) Snapshot.takeMutableSnapshot() 📸 1

    2 dog.name.value = "Fido" Dog(name = "Fido") snapshot.enter { dog.name.value = "Taco" } Dog(name = "Spot")
  35. #hg2compose @mvndy @jossiwolf Snapshots 📸 (and dogs🐕) Snapshot.takeMutableSnapshot() 📸 1

    2 dog.name.value = "Fido" Dog(name = "Fido") snapshot.enter { dog.name.value = "Taco" } Dog(name = "Spot")
  36. #hg2compose @mvndy @jossiwolf Snapshots 📸 (and dogs🐕) Snapshot.takeMutableSnapshot() 📸 1

    2 dog.name.value = "Fido" Dog(name = "Fido") snapshot.enter { dog.name.value = "Taco" } Dog(name = "Spot") Dog(name = "Taco") ✔
  37. #hg2compose @mvndy @jossiwolf Snapshots 📸 (and dogs🐕) snapshot.apply() Snapshot.takeMutableSnapshot() dog.name.value

    = "Fido" 📸 1 2 snapshot.enter { dog.name.value = "Taco" } Dog(name = "Spot") Dog(name = "Taco") Dog(name = "Fido") Dog(name = "Taco")
  38. #hg2compose @mvndy @jossiwolf Snapshots 📸 (and dogs🐕) snapshot.apply() Snapshot.takeMutableSnapshot() dog.name.value

    = "Fido" 📸 1 2 snapshot.enter { dog.name.value = "Taco" } Dog(name = "Spot") Dog(name = "Taco") Dog(name = "Fido") Dog(name = "Taco")
  39. #hg2compose @mvndy @jossiwolf https://dev.to/zachklipp/introduction-to-the-compose-snapshot-system-19cn fun main() { val dog =

    Dog() dog.name.value = "Spot" val snapshot = Snapshot.takeSnapshot( readObserver = { value -> ... } ) dog.name.value = "Fido" println(dog.name.value) // Fido snapshot.enter { println(dog.name.value) // Spot } } Snapshots 📸 (and dogs🐕)
  40. #hg2compose @mvndy @jossiwolf fun main() { val dog = Dog()

    dog.name.value = "Spot" val snapshot = Snapshot.takeSnapshot( readObserver = { value -> println(value) // MutableState(value=Spot)@ ... } ) dog.name.value = "Fido" println(dog.name.value) // Fido snapshot.enter { println(dog.name.value) // Spot } } Snapshots 📸 (and dogs🐕)
  41. #hg2compose @mvndy @jossiwolf fun main() { val dog = Dog()

    dog.name.value = "Spot" val snapshot = Snapshot.takeSnapshot( readObserver = { value -> println(value) // MutableState(value=Spot)@ ... } ) dog.name.value = "Fido" println(dog.name.value) // Fido snapshot.enter { println(dog.name.value) // Spot } } Snapshots 📸 (and dogs🐕)
  42. #hg2compose @mvndy @jossiwolf fun main() { val dog = Dog()

    dog.name.value = "Spot" val snapshot = Snapshot.takeMutableSnapshot( readObserver = { value -> println(value) // MutableState(value=Spot)@ ... }, writeObserver = { value -> ... } ) dog.name.value = "Fido" snapshot.enter { dog.name.value = "Taco" } } Snapshots 📸 (and dogs🐕)
  43. (Snapshot) State #hg2compose @mvndy_hd @jossiwolf

  44. #hg2compose interface State<out T> { val value: T } package

    androidx.compose.runtime @mvndy_hd @jossiwolf
  45. #hg2compose interface State<out T> { val value: T } interface

    MutableState<T> : State<T> { override var value: T operator fun component1(): T operator fun component2(): (T) -> Unit } package androidx.compose.runtime @mvndy_hd @jossiwolf
  46. #hg2compose /** * Return the current readable state record for

    the [snapshot]. * It is assumed that [this] is the first record of [state] */ fun <T : StateRecord> T.readable(state: StateObject, snapshot: Snapshot): T { // invoke the observer associated with the current snapshot. snapshot.readObserver ?. invoke(state) return readable(this, snapshot.id, snapshot.invalid) ?: readError() } package androidx.compose.runtime @mvndy_hd @jossiwolf
  47. #hg2compose /** * Return the current readable state record for

    the [snapshot]. * It is assumed that [this] is the first record of [state] */ fun <T : StateRecord> T.readable(state: StateObject, snapshot: Snapshot): T { // invoke the observer associated with the current snapshot. snapshot.readObserver ?. invoke(state) return readable(this, snapshot.id, snapshot.invalid) ?: readError() } package androidx.compose.runtime @mvndy_hd @jossiwolf
  48. @Composable fun HitchhikerApp() { var name = remember { mutableStateOf("Bob")

    } HelloWorld(greeting = name.value) } @Composable fun HelloWorld(greeting: String) { println("Hello, $greeting!") } #hg2compose @mvndy_hd @jossiwolf
  49. #hg2compose suspend fun runApp() { val composer = Recomposer() GlobalSnapshotManager.ensureStarted()

    launch(DefaultChoreographerFrameClock) { composer.runRecomposeAndApplyChanges() } Composition(NodeApplier(RootNode()), composer).apply { setContent { Content() } } } @mvndy_hd @jossiwolf
  50. #hg2compose suspend fun runApp() { val composer = Recomposer() GlobalSnapshotManager.ensureStarted()

    launch(DefaultChoreographerFrameClock) { composer.runRecomposeAndApplyChanges() } Composition(NodeApplier(RootNode()), composer).apply { setContent { Content() } } } medium.com/@takahirom/inside-jetpack-compose-2e971675e55e @mvndy_hd @jossiwolf
  51. Recomposers manage the Snapshot and Compositions, i.e. composing initially and

    recomposing. #hg2compose Glossary @mvndy_hd @jossiwolf
  52. #hg2compose suspend fun runApp() { val composer = Recomposer() GlobalSnapshotManager.ensureStarted()

    launch(DefaultChoreographerFrameClock) { composer.runRecomposeAndApplyChanges() } Composition(NodeApplier(RootNode()), composer).apply { setContent { Content() } } } medium.com/@takahirom/inside-jetpack-compose-2e971675e55e @mvndy_hd @jossiwolf
  53. #hg2compose suspend fun runApp() { val composer = Recomposer() GlobalSnapshotManager.ensureStarted()

    launch(DefaultChoreographerFrameClock) { composer.runRecomposeAndApplyChanges() } Composition(NodeApplier(RootNode()), composer).apply { setContent { Content() } } } medium.com/@takahirom/inside-jetpack-compose-2e971675e55e @mvndy_hd @jossiwolf
  54. #hg2compose suspend fun runApp() { val composer = Recomposer() GlobalSnapshotManager.ensureStarted()

    launch(DefaultChoreographerFrameClock) { composer.runRecomposeAndApplyChanges() } Composition(NodeApplier(RootNode()), composer).apply { setContent { Content() } } } medium.com/@takahirom/inside-jetpack-compose-2e971675e55e @mvndy_hd @jossiwolf
  55. #hg2compose suspend fun runApp() { val composer = Recomposer() GlobalSnapshotManager.ensureStarted()

    launch(DefaultChoreographerFrameClock) { composer.runRecomposeAndApplyChanges() } Composition(NodeApplier(RootNode()), composer).apply { setContent { Content() } } } medium.com/@takahirom/inside-jetpack-compose-2e971675e55e @mvndy_hd @jossiwolf
  56. #hg2compose private inline fun <T> composing( composition: ControlledComposition, modifiedValues: IdentityArraySet<Any>?,

    block: () -> T ): T { val snapshot = Snapshot.takeMutableSnapshot( readObserverOf(composition), writeObserverOf(composition, modifiedValues) ) try { return snapshot.enter(block) } finally { applyAndCheck(snapshot) } } Recomposer @mvndy_hd @jossiwolf
  57. #hg2compose private inline fun <T> composing( composition: ControlledComposition, modifiedValues: IdentityArraySet<Any>?,

    block: () -> T ): T { val snapshot = Snapshot.takeMutableSnapshot( readObserverOf(composition), writeObserverOf(composition, modifiedValues) ) try { return snapshot.enter(block) } finally { applyAndCheck(snapshot) } } Recomposer @mvndy_hd @jossiwolf
  58. #hg2compose private inline fun <T> composing( composition: ControlledComposition, modifiedValues: IdentityArraySet<Any>?,

    block: () -> T ): T { val snapshot = Snapshot.takeMutableSnapshot( readObserverOf(composition), writeObserverOf(composition, modifiedValues) ) try { return snapshot.enter(block) } finally { applyAndCheck(snapshot) } } Recomposer @mvndy_hd @jossiwolf
  59. #hg2compose private fun readObserverOf( composition: ControlledComposition ): (Any) -> Unit

    { return { value -> composition.recordReadOf(value) } } Recomposer @mvndy_hd @jossiwolf
  60. #hg2compose // Invalidate any recompose scopes that read this value.

    observations.forEachScopeOf(value) { scope -> if (scope.invalidateForResult(value) == InvalidationResult.IMMINENT) { observationsProcessed.add(value, scope) } } Composition @mvndy_hd @jossiwolf
  61. #hg2compose RecomposeScope /** * Invalidate the group which will cause

    [composition] to request * this scope be recomposed */ fun invalidateForResult(value: Any?): InvalidationResult = composition ?. invalidate(this, value) ?: InvalidationResult.IGNORED @mvndy_hd @jossiwolf
  62. @Composable fun HitchhikerApp() { var name = remember { mutableStateOf("Bob")

    } HelloWorld(greeting = name.value) } @Composable fun HelloWorld(greeting: String) { println("Hello, $greeting!") } #hg2compose S
 N
 A
 P
 S
 H
 O
 T @mvndy_hd @jossiwolf
  63. @Composable fun HelloWorld(greeting: String) { Text("Hello, $greeting!") } #hg2compose @mvndy_hd

    @jossiwolf
  64. @Composable fun HelloWorld(greeting: String, %composer: Composer?, %changed: Int) { %composer

    = %composer.startRestartGroup( <> ) val %dirty = %changed if (%changed and 0b1110 === 0) { %dirty = %dirty or if (%composer.changed(greeting)) 0b0100 else 0b0010 } if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) { Text("Hello, $greeting!") } else { %composer.skipToGroupEnd() } %composer.endRestartGroup() ?. updateScope { %composer: Composer?, %force: Int -> HelloWorld(greeting, %composer, %changed or 0b0001) } } #hg2compose @mvndy_hd @jossiwolf
  65. @Composable fun HelloWorld(greeting: String, %composer: Composer?, %changed: Int) { %composer

    = %composer.startRestartGroup( <> ) val %dirty = %changed if (%changed and 0b1110 === 0) { %dirty = %dirty or if (%composer.changed(greeting)) 0b0100 else 0b0010 } if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) { Text("Hello, $greeting!") } else { %composer.skipToGroupEnd() } %composer.endRestartGroup() ?. updateScope { %composer: Composer?, %force: Int -> HelloWorld(greeting, %composer, %changed or 0b0001) } } #hg2compose @mvndy_hd @jossiwolf
  66. @Composable fun HelloWorld(greeting: String, %composer: Composer?, %changed: Int) { %composer

    = %composer.startRestartGroup( <> ) val %dirty = %changed if (%changed and 0b1110 === 0) { %dirty = %dirty or if (%composer.changed(greeting)) 0b0100 else 0b0010 } if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) { Text("Hello, $greeting!") } else { %composer.skipToGroupEnd() } %composer.endRestartGroup() ?. updateScope { %composer: Composer?, %force: Int -> HelloWorld(greeting, %composer, %changed or 0b0001) } } #hg2compose @mvndy_hd @jossiwolf
  67. @Composable fun HelloWorld(greeting: String, %composer: Composer?, %changed: Int) { %composer

    = %composer.startRestartGroup( <> ) val %dirty = %changed if (%changed and 0b1110 === 0) { %dirty = %dirty or if (%composer.changed(greeting)) 0b0100 else 0b0010 } if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) { Text("Hello, $greeting!") } else { %composer.skipToGroupEnd() } %composer.endRestartGroup() ?. updateScope { %composer: Composer?, %force: Int -> HelloWorld(greeting, %composer, %changed or 0b0001) } } #hg2compose @mvndy_hd @jossiwolf
  68. @Composable fun HelloWorld(greeting: String, %composer: Composer?, %changed: Int) { %composer

    = %composer.startRestartGroup( <> ) val %dirty = %changed if (%changed and 0b1110 === 0) { %dirty = %dirty or if (%composer.changed(greeting)) 0b0100 else 0b0010 } if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) { Text("Hello, $greeting!") } else { %composer.skipToGroupEnd() } %composer.endRestartGroup() ?. updateScope { %composer: Composer?, %force: Int -> HelloWorld(greeting, %composer, %changed or 0b0001) } } #hg2compose @mvndy_hd @jossiwolf
  69. @Composable fun HelloWorld(greeting: String, %composer: Composer?, %changed: Int) { %composer

    = %composer.startRestartGroup( <> ) val %dirty = %changed if (%changed and 0b1110 === 0) { %dirty = %dirty or if (%composer.changed(greeting)) 0b0100 else 0b0010 } if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) { Text("Hello, $greeting!") } else { %composer.skipToGroupEnd() } %composer.endRestartGroup() ?. updateScope { %composer: Composer?, %force: Int -> HelloWorld(greeting, %composer, %changed or 0b0001) } } #hg2compose @mvndy_hd @jossiwolf
  70. @Composable fun HelloWorld(greeting: String, %composer: Composer?, %changed: Int) { %composer

    = %composer.startRestartGroup( <> ) val %dirty = %changed if (%changed and 0b1110 === 0) { %dirty = %dirty or if (%composer.changed(greeting)) 0b0100 else 0b0010 } if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) { Text("Hello, $greeting!") } else { %composer.skipToGroupEnd() } %composer.endRestartGroup() ?. updateScope { %composer: Composer?, %force: Int -> HelloWorld(greeting, %composer, %changed or 0b0001) } } #hg2compose @mvndy_hd @jossiwolf
  71. Restartable Groups @Composable fun HelloWorld(greeting: String) { Text("Hello, $greeting!") }

    @mvndy_hd @jossiwolf
  72. @Composable fun HelloWorld(names: List<String>) { names.forEach { name -> key(name)

    { Text(name) } } } Moveable Groups @mvndy_hd @jossiwolf
  73. @Composable fun HelloWorld(name: String) { if (name == "Amanda") {

    Text("Hello!") else if (name == "Jossi") { Text("Hallo!") } else { Text("Uhm .. hi?") } } Replaceable Groups @mvndy_hd @jossiwolf
  74. #hg2compose .kt Lexer (Scanner) Syntax Analysis Parsing Phase Generates tokens

    - Generates tokens to create an AST - PSI lays over AST Resolution Codegen Resolution performed on AST tree - PSI gets enhanced with descriptors - Symbol table generated; associated node w/ descriptor via BindingTrace Frontend Backend - Takes optimized IR and performs more analysis, transformations + optimisations speci fi c to target CPU architecture - Multithreading Machine-Dependent Optimisations IR Transform Compiler Analysis Middle End “Lower” - Performs optimisations on IR - Removes dead code - Refactors the code - Improves performance - Analyzes IR for data needed to create - Call graph - Control- fl ow graph < > Bytecode Target Program FUN BLOCK_BODY CALL VAR IR Unoptimized IR Codegen IR to Bytecode 
 Optimized IR FUN BLOCK_BODY CALL VAR IR Backend The Kotlin compiler @mvndy_hd @jossiwolf
  75. #hg2compose .kt Lexer (Scanner) Syntax Analysis Parsing Phase Generates tokens

    - Generates tokens to create an AST - PSI lays over AST Resolution Codegen Resolution performed on AST tree - PSI gets enhanced with descriptors - Symbol table generated; associated node w/ descriptor via BindingTrace Frontend Backend - Takes optimized IR and performs more analysis, transformations + optimisations speci fi c to target CPU architecture - Multithreading Machine-Dependent Optimisations IR Transformations Compiler Analysis Middle End “Lower” - Performs optimisations on IR - Removes dead code - Refactors the code - Improves performance - Analyzes IR for data needed to create - Call graph - Control- fl ow graph < > Bytecode Target Program FUN BLOCK_BODY CALL VAR IR Unoptimized IR Codegen IR to Bytecode 
 Optimized IR FUN BLOCK_BODY CALL VAR IR Backend Compose compiler plugin Backend - Takes optimized IR and performs more analysis, transformations + optimisations speci fi c to target CPU architecture - Multithreading Machine-Dependent Optimisations IR Transform Compiler Analysis Middle End “Lower” - Performs optimisations on IR - Removes dead code - Refactors the code - Improves performance - Analyzes IR for data needed to create - Call graph - Control- fl ow graph < > Bytecode Target Program FUN BLOCK_BODY CALL VAR IR Unoptimized IR Codegen IR to Bytecode 
 Optimized IR FUN BLOCK_BODY CALL VAR IR Backend @mvndy_hd @jossiwolf
  76. #hg2compose .kt Lexer (Scanner) Syntax Analysis Parsing Phase Generates tokens

    - Generates tokens to create an AST - PSI lays over AST Semantic Analysis Codegen Semantic Analysis performed on AST - PSI gets enhanced with descriptors - Symbol table generated; associated node w/ descriptor via BindingTrace Kotlin Compiler Frontend @mvndy_hd @jossiwolf
  77. #hg2compose .kt Lexer (Scanner) Syntax Analysis Parsing Phase Generates tokens

    - Generates tokens to create an AST - PSI lays over AST Resolution Codegen Semantic Analysis performed on AST - PSI gets enhanced with descriptors - Symbol table generated; associated node w/ descriptor via BindingTrace Kotlin Compiler Frontend @mvndy_hd @jossiwolf
  78. Element.FUN Element.BLOCK Element.CALL_EXPRESSION Token.L_BRACE Token.R_BRACE Element.REFERENCE_EXPRESSION Element.VALUE_ARGUMENT_LIST Token.IDENTIFIER Token.LPAR Token.RPAR

    Element.STRING_TEMPLATE Element.LITERAL_STRING_TEMPLATE_ENTRY Token.OPEN_QUOTE Element.REGULAR_STRING_PART Token.OPEN_QUOTE #hg2compose @mvndy_hd @jossiwolf Text("Hello, World!") AST
  79. #hg2compose .kt Lexer (Scanner) Syntax Analysis Parsing Phase Generates tokens

    - Generates tokens to create an AST - PSI lays over AST Resolution Codegen Semantic Analysis performed on AST - PSI gets enhanced with descriptors - Symbol table generated; associated node w/ descriptor via BindingTrace Kotlin Compiler Frontend @mvndy_hd @jossiwolf
  80. KtBlockExpression KtCallExpression KtReferenceExpression KtValueArgument Token.IDENTIFIER Element.REGULAR_STRING_PART #hg2compose @mvndy_hd @jossiwolf Text("Hello,

    World!") KtValueArgument KtStringTemplateExpression PSI
  81. #hg2compose .kt Lexer (Scanner) Syntax Analysis Parsing Phase Generates tokens

    - Generates tokens to create an AST - PSI lays over AST Resolution Codegen Semantic Analysis performed on AST - PSI gets enhanced with descriptors - Symbol table generated; associated node w/ descriptor via BindingTrace Kotlin Compiler Frontend @mvndy_hd @jossiwolf
  82. #hg2compose func() IR Transform Compiler Analysis Middle End “Lower” -

    Performs optimisations on IR - Removes dead code - Refactors the code - Improves performance - Analyzes IR for data needed to create - Call graph - Control- fl ow graph Backend - Takes optimized IR and performs more analysis, transformations + optimisations speci fi c to target CPU architecture - Multithreading func() Optimized IR < > Bytecode Target Program @mvndy_hd @jossiwolf
  83. #hg2compose func() IR Transform Compiler Analysis Middle End “Lower” -

    Performs optimisations on IR - Removes dead code - Refactors the code - Improves performance - Analyzes IR for data needed to create - Call graph - Control- fl ow graph Backend - Takes optimized IR and performs more analysis, transformations + optimisations speci fi c to target CPU architecture - Multithreading func() Optimized IR < > Bytecode Target Program @mvndy_hd @jossiwolf
  84. FUN name:HelloWorld visibility:public modality:FINAL <> (greeting:kotlin.String, $composer:androidx.compose.runtime.Composer?, $changed:kotlin.Int) returnType:kotlin.Unit annotations:

    Composable VALUE_PARAMETER name:greeting index:0 type:kotlin.String VALUE_PARAMETER name:$composer index:1 type:androidx.compose.runtime.Composer? [assignable] VALUE_PARAMETER name:$changed index:2 type:kotlin.Int BLOCK_BODY BLOCK type=kotlin.Unit origin=null SET_VAR '$composer: androidx.compose.runtime.Composer? [assignable] declared in <root>.HelloWorld' type=kotlin.Unit origin=null CALL 'public abstract fun startRestartGroup (key: kotlin.Int): androidx.compose.runtime.Composer declared in androidx.compose.runtime.Composer' type=androidx.compose.runtime.Composer origin=null $this: GET_VAR '$composer: androidx.compose.runtime.Composer? [assignable] declared in <root>.HelloWorld' type=androidx.compose.runtime.Composer? origin=null key: CONST Int type=kotlin.Int value=734305942 CALL 'public final fun sourceInformation (composer: androidx.compose.runtime.Composer, sourceInformation: kotlin.String): kotlin.Unit declared in androidx.compose.runtime.ComposerKt' type=kotlin.Unit origin=null composer: GET_VAR '$composer: androidx.compose.runtime.Composer? [assignable] declared in <root>.HelloWorld' type=androidx.compose.runtime.Composer? origin=null sourceInformation: CONST String type=kotlin.String value="C(HelloWorld):Test.kt" WHEN type=kotlin.Unit origin=IF BRANCH if: WHEN type=kotlin.Boolean origin=OROR BRANCH if: CALL 'public final fun not (): kotlin.Boolean [operator] declared in kotlin.Boolean' type=kotlin.Boolean origin=null $this: CALL 'public final fun EQEQEQ (arg0: kotlin.Any?, arg1: kotlin.Any?): kotlin.Boolean declared in kotlin.internal.ir' type=kotlin.Boolean origin=null arg0: CALL 'public final fun and (other: kotlin.Int): kotlin.Int [infix] declared in kotlin.Int' type=kotlin.Int origin=null $this: GET_VAR '$changed: kotlin.Int declared in <root>.HelloWorld' type=kotlin.Int origin=null other: CONST Int type=kotlin.Int value=1 arg1: CONST Int type=kotlin.Int value=0 then: CONST Boolean type=kotlin.Boolean value=true BRANCH if: CONST Boolean type=kotlin.Boolean value=true then: CALL 'public final fun not (): kotlin.Boolean [operator] declared in kotlin.Boolean' type=kotlin.Boolean origin=null $this: CALL 'public abstract fun <get-skipping> (): kotlin.Boolean declared in androidx.compose.runtime.Composer' type=kotlin.Boolean origin=null $this: GET_VAR '$composer: androidx.compose.runtime.Composer? [assignable] declared in <root>.HelloWorld' type=androidx.compose.runtime.Composer? origin=null then: BLOCK type=kotlin.Unit origin=null CALL 'public final fun println (message: kotlin.Any?): kotlin.Unit [inline] declared in kotlin.io.ConsoleKt' type=kotlin.Unit origin=null message: CONST String type=kotlin.String value="Hello" BRANCH if: CONST Boolean type=kotlin.Boolean value=true then: CALL 'public abstract fun skipToGroupEnd (): kotlin.Unit declared in androidx.compose.runtime.Composer' type=kotlin.Unit origin=null $this: GET_VAR '$composer: androidx.compose.runtime.Composer? [assignable] declared in <root>.HelloWorld' type=androidx.compose.runtime.Composer? origin=null BLOCK type=kotlin.Unit origin=null BLOCK type=kotlin.Unit origin=SAFE_CALL VAR IR_TEMPORARY_VARIABLE name:tmp0_safe_receiver type:androidx.compose.runtime.ScopeUpdateScope? [val] CALL 'public abstract fun endRestartGroup (): androidx.compose.runtime.ScopeUpdateScope? declared in androidx.compose.runtime.Composer' type=androidx.compose.runtime.ScopeUpdateScope? origin=null $this: GET_VAR '$composer: androidx.compose.runtime.Composer? [assignable] declared in <root>.HelloWorld' type=androidx.compose.runtime.Composer? origin=null WHEN type=kotlin.Unit origin=IF BRANCH if: CALL 'public final fun EQEQEQ (arg0: kotlin.Any?, arg1: kotlin.Any?): kotlin.Boolean declared in kotlin.internal.ir' type=kotlin.Boolean origin=null arg0: GET_VAR 'val tmp0_safe_receiver: androidx.compose.runtime.ScopeUpdateScope? [val] declared in <root>.HelloWorld' type=androidx.compose.runtime.ScopeUpdateScope? origin=null arg1: CONST Null type=kotlin.Any? value=null then: CONST Null type=kotlin.Any? value=null BRANCH if: CONST Boolean type=kotlin.Boolean value=true then: CALL 'public abstract fun updateScope (block: kotlin.Function2<androidx.compose.runtime.Composer, kotlin.Int, kotlin.Unit>): kotlin.Unit declared in androidx.compose.runtime.ScopeUpdateScope' type=kotlin.Unit origin=null $this: GET_VAR 'val tmp0_safe_receiver: androidx.compose.runtime.ScopeUpdateScope? [val] declared in <root>.HelloWorld' type=androidx.compose.runtime.ScopeUpdateScope? origin=null block: BLOCK type=kotlin.Function2<androidx.compose.runtime.Composer, kotlin.Int, kotlin.Unit> origin=LAMBDA FUN LOCAL_FUNCTION_FOR_LAMBDA name : < anonymous> visibility:local modality:FINAL < > ($composer:androidx.compose.runtime.Composer?, $force:kotlin.Int) returnType:kotlin.Unit VALUE_PARAMETER name:$composer index:0 type:androidx.compose.runtime.Composer? VALUE_PARAMETER name:$force index:1 type:kotlin.Int BLOCK_BODY RETURN type=kotlin.Nothing from='local final fun <anonymous> ($composer: androidx.compose.runtime.Composer?, $force: kotlin.Int): kotlin.Unit declared in <root>.HelloWorld' CALL 'public final fun HelloWorld (greeting: kotlin.String, $composer: androidx.compose.runtime.Composer?, $changed: kotlin.Int): kotlin.Unit declared in <root>' type=kotlin.Unit origin=null greeting: GET_VAR 'greeting: kotlin.String declared in <root>.HelloWorld' type=kotlin.String origin=null $composer: GET_VAR '$composer: androidx.compose.runtime.Composer? declared in <root>.HelloWorld.<anonymous>' type=androidx.compose.runtime.Composer? origin=null $changed: CALL 'public final fun or (other: kotlin.Int): kotlin.Int [infix] declared in kotlin.Int' type=kotlin.Int origin=null $this: GET_VAR '$changed: kotlin.Int declared in <root>.HelloWorld' type=kotlin.Int origin=null other: CONST Int type=kotlin.Int value=1 @mvndy_hd @jossiwolf
  85. @Composable fun HelloWorld() { Text("Hello World!") } #hg2compose @mvndy_hd @jossiwolf

  86. @Composable fun HelloWorld(%composer: Composer?, %changed: Int) { %composer = %composer.startRestartGroup(

    <> ) if (%changed !== 0 || !%composer.skipping) { Text("Hello World”) } else { %composer.skipToGroupEnd() } %composer.endRestartGroup()? .updateScope { %composer: Composer?, %force: Int -> HelloWorld(%composer, %changed or 0b0001) } } #hg2compose @mvndy_hd @jossiwolf
  87. A Composer connects Composables to Runtime, keeps track of groups

    that are used to build the Slot Table and positions it. #hg2compose @mvndy_hd @jossiwolf
  88. #hg2compose var name by remember { mutableStateOf("Bob") } @Composable fun

    Greeting(greeting: String) { Text("Hello, $greeting!") } Greeting(name) name = "Jossi" // Hello, Bob! // Hello, Jossi! Slot Table by Example @mvndy_hd @jossiwolf
  89. #hg2compose EMPTY EMPTY Group(4) State(“Bob”) var name by remember {

    mutableStateOf("Bob") } Slot Table by Example EMPTY EMPTY EMPTY EMPTY EMPTY @mvndy_hd @jossiwolf
  90. #hg2compose Group(4) State(“Bob”) Group(5) var name by remember { mutableStateOf("Bob")

    } Slot Table by Example EMPTY EMPTY EMPTY EMPTY EMPTY EMPTY @Composable fun Greeting(greeting: String) { } Greeting(name) @mvndy_hd @jossiwolf
  91. #hg2compose EMPTY EMPTY Group(4) State(“Bob”) Group(5) Group(6) Group(7) Group(8) “Hello,

    Bob!” var name by remember { mutableStateOf("Bob") } Slot Table by Example @Composable fun Greeting(greeting: String) { Text("Hello, $greeting!”) } Greeting(name) @mvndy_hd @jossiwolf
  92. #hg2compose EMPTY EMPTY Group(4) State(“Bob”) Group(5) Group(6) Group(7) Group(8) “Hello,

    Jossi!” var name by remember { mutableStateOf("Bob") } @Composable fun Greeting(greeting: String) { Text("Hello, $greeting!”) } Greeting(name) name = "Jossi" Slot Table by Example @mvndy_hd @jossiwolf
  93. UI #hg2compose @mvndy_hd @jossiwolf

  94. #hg2compose Text(text = name) Layout Layout name by remember {

    mutableStateOf("Bob") } Bob @mvndy_hd @jossiwolf
  95. #hg2compose @mvndy_hd @jossiwolf interface Applier<N> { val current: N fun

    onBeginChanges() {} fun onEndChanges() {} fun down(node: N) fun up() fun insertTopDown(index: Int, instance: N) fun insertBottomUp(index: Int, instance: N) fun remove(index: Int, count: Int) fun move(from: Int, to: Int, count: Int) fun clear() }
  96. #hg2compose Text(text = name) Layout Layout @mvndy_hd @jossiwolf LayoutNode LayoutNode

    LayoutNode
  97. 🏃Efficient Codegen Tips🏃 #hg2compose The point of Compose transformations is

    to reduce as many groups + group executions as possible - but you can help too! @mvndy_hd @jossiwolf
  98. #hg2compose Use key to make recomposition of lists cheaper! This

    helps Compose generate moveable groups, avoiding recomposition i.e. when the position of a list item changes. @mvndy_hd @jossiwolf
  99. #hg2compose Make sure to read state values at the lowest

    node of the tree possible You only want to recompose where you actually need the state :) @mvndy_hd @jossiwolf
  100. • Jorge Castillo for writing "Compose Internals" • Leland Richardson

    for sitting down with us and patiently answering our questions! #hg2compose Thank you!!! @mvndy_hd @jossiwolf
  101. #hg2compose linktr.ee/hg2compose @mvndy_hd @jossiwolf