Slide 1

Slide 1 text

Hitchhiker’s Guide to Compose Snapshots, Compiler Plugins and Appliers @mvndy_hd @jossiwolf #hg2compose

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

"Jetpack Compose is Android's modern toolkit for building native UI." #hg2compose - developer.android.com @mvndy_hd @jossiwolf

Slide 14

Slide 14 text

Compiler Runtime UI #hg2compose != @mvndy_hd @jossiwolf

Slide 15

Slide 15 text

A Composable is a restartable function that emits nodes into a tree. #hg2compose Glossary @mvndy_hd @jossiwolf

Slide 16

Slide 16 text

Recomposition is the process of re-executing a Composable function to produce an updated slot table #hg2compose Glossary @mvndy_hd @jossiwolf

Slide 17

Slide 17 text

The Composition holds the Slot Table, invalidations and a Composer. #hg2compose Glossary @mvndy_hd @jossiwolf

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

Recomposers manage the Snapshot and Compositions #hg2compose Glossary @mvndy_hd @jossiwolf

Slide 21

Slide 21 text

@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

Slide 22

Slide 22 text

Snapshots 📸 #hg2compose @mvndy_hd @jossiwolf

Slide 23

Slide 23 text

"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

Slide 24

Slide 24 text

#hg2compose https://dev.to/zachklipp/introduction-to-the-compose-snapshot-system-19cn class Dog { var name: MutableState = 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

Slide 25

Slide 25 text

#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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

#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

Slide 32

Slide 32 text

#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

Slide 33

Slide 33 text

#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

Slide 34

Slide 34 text

#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")

Slide 35

Slide 35 text

#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")

Slide 36

Slide 36 text

#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") ✔

Slide 37

Slide 37 text

#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")

Slide 38

Slide 38 text

#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")

Slide 39

Slide 39 text

#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🐕)

Slide 40

Slide 40 text

#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🐕)

Slide 41

Slide 41 text

#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🐕)

Slide 42

Slide 42 text

#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🐕)

Slide 43

Slide 43 text

(Snapshot) State #hg2compose @mvndy_hd @jossiwolf

Slide 44

Slide 44 text

#hg2compose interface State { val value: T } package androidx.compose.runtime @mvndy_hd @jossiwolf

Slide 45

Slide 45 text

#hg2compose interface State { val value: T } interface MutableState : State { override var value: T operator fun component1(): T operator fun component2(): (T) -> Unit } package androidx.compose.runtime @mvndy_hd @jossiwolf

Slide 46

Slide 46 text

#hg2compose /** * Return the current readable state record for the [snapshot]. * It is assumed that [this] is the first record of [state] */ fun 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

Slide 47

Slide 47 text

#hg2compose /** * Return the current readable state record for the [snapshot]. * It is assumed that [this] is the first record of [state] */ fun 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

Slide 48

Slide 48 text

@Composable fun HitchhikerApp() { var name = remember { mutableStateOf("Bob") } HelloWorld(greeting = name.value) } @Composable fun HelloWorld(greeting: String) { println("Hello, $greeting!") } #hg2compose @mvndy_hd @jossiwolf

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

#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

Slide 51

Slide 51 text

Recomposers manage the Snapshot and Compositions, i.e. composing initially and recomposing. #hg2compose Glossary @mvndy_hd @jossiwolf

Slide 52

Slide 52 text

#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

Slide 53

Slide 53 text

#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

Slide 54

Slide 54 text

#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

Slide 55

Slide 55 text

#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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

#hg2compose private fun readObserverOf( composition: ControlledComposition ): (Any) -> Unit { return { value -> composition.recordReadOf(value) } } Recomposer @mvndy_hd @jossiwolf

Slide 60

Slide 60 text

#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

Slide 61

Slide 61 text

#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

Slide 62

Slide 62 text

@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

Slide 63

Slide 63 text

@Composable fun HelloWorld(greeting: String) { Text("Hello, $greeting!") } #hg2compose @mvndy_hd @jossiwolf

Slide 64

Slide 64 text

@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

Slide 65

Slide 65 text

@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

Slide 66

Slide 66 text

@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

Slide 67

Slide 67 text

@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

Slide 68

Slide 68 text

@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

Slide 69

Slide 69 text

@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

Slide 70

Slide 70 text

@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

Slide 71

Slide 71 text

Restartable Groups @Composable fun HelloWorld(greeting: String) { Text("Hello, $greeting!") } @mvndy_hd @jossiwolf

Slide 72

Slide 72 text

@Composable fun HelloWorld(names: List) { names.forEach { name -> key(name) { Text(name) } } } Moveable Groups @mvndy_hd @jossiwolf

Slide 73

Slide 73 text

@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

Slide 74

Slide 74 text

#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

Slide 75

Slide 75 text

#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

Slide 76

Slide 76 text

#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

Slide 77

Slide 77 text

#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

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

#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

Slide 80

Slide 80 text

KtBlockExpression KtCallExpression KtReferenceExpression KtValueArgument Token.IDENTIFIER Element.REGULAR_STRING_PART #hg2compose @mvndy_hd @jossiwolf Text("Hello, World!") KtValueArgument KtStringTemplateExpression PSI

Slide 81

Slide 81 text

#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

Slide 82

Slide 82 text

#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

Slide 83

Slide 83 text

#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

Slide 84

Slide 84 text

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 .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 .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 .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 .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 (): kotlin.Boolean declared in androidx.compose.runtime.Composer' type=kotlin.Boolean origin=null $this: GET_VAR '$composer: androidx.compose.runtime.Composer? [assignable] declared in .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 .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 .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 .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): 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 .HelloWorld' type=androidx.compose.runtime.ScopeUpdateScope? origin=null block: BLOCK type=kotlin.Function2 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 ($composer: androidx.compose.runtime.Composer?, $force: kotlin.Int): kotlin.Unit declared in .HelloWorld' CALL 'public final fun HelloWorld (greeting: kotlin.String, $composer: androidx.compose.runtime.Composer?, $changed: kotlin.Int): kotlin.Unit declared in ' type=kotlin.Unit origin=null greeting: GET_VAR 'greeting: kotlin.String declared in .HelloWorld' type=kotlin.String origin=null $composer: GET_VAR '$composer: androidx.compose.runtime.Composer? declared in .HelloWorld.' 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 .HelloWorld' type=kotlin.Int origin=null other: CONST Int type=kotlin.Int value=1 @mvndy_hd @jossiwolf

Slide 85

Slide 85 text

@Composable fun HelloWorld() { Text("Hello World!") } #hg2compose @mvndy_hd @jossiwolf

Slide 86

Slide 86 text

@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

Slide 87

Slide 87 text

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

Slide 88

Slide 88 text

#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

Slide 89

Slide 89 text

#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

Slide 90

Slide 90 text

#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

Slide 91

Slide 91 text

#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

Slide 92

Slide 92 text

#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

Slide 93

Slide 93 text

UI #hg2compose @mvndy_hd @jossiwolf

Slide 94

Slide 94 text

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

Slide 95

Slide 95 text

#hg2compose @mvndy_hd @jossiwolf interface Applier { 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() }

Slide 96

Slide 96 text

#hg2compose Text(text = name) Layout Layout @mvndy_hd @jossiwolf LayoutNode LayoutNode LayoutNode

Slide 97

Slide 97 text

🏃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

Slide 98

Slide 98 text

#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

Slide 99

Slide 99 text

#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

Slide 100

Slide 100 text

• Jorge Castillo for writing "Compose Internals" • Leland Richardson for sitting down with us and patiently answering our questions! #hg2compose Thank you!!! @mvndy_hd @jossiwolf

Slide 101

Slide 101 text

#hg2compose linktr.ee/hg2compose @mvndy_hd @jossiwolf