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

[Berlin 2021] Hitchhiker's Guide to Compose

Jossi Wolf
October 20, 2021

[Berlin 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. Make sure to check out 2022's updated version on our profiles!

Jossi Wolf

October 20, 2021
Tweet

More Decks by Jossi Wolf

Other Decks in Technology

Transcript

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  14. Compiler Runtime UI
    #hg2compose
    !=
    @mvndy_hd @jossiwolf

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

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

    View Slide

  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

    View Slide

  22. Snapshots 📸
    #hg2compose
    @mvndy_hd @jossiwolf

    View Slide

  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

    View Slide

  24. #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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  30. #hg2compose
    Snapshot.takeSnapshot()
    dog.name.value = "Fido"
    📸
    1
    2 Dog(name = "Spot")
    Dog(name = "Fido")
    snapshot.enter {

    .
    .
    .


    }

    Snapshots 📸
    (and dogs🐕)
    @mvndy_hd @jossiwolf

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  43. (Snapshot) State
    #hg2compose
    @mvndy_hd @jossiwolf

    View Slide

  44. #hg2compose
    interface State {

    val value: T

    }
    package androidx.compose.runtime
    @mvndy_hd @jossiwolf

    View Slide

  45. #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

    View Slide

  46. #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

    View Slide

  47. #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

    View Slide

  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

    View Slide

  49. #hg2compose
    suspend fun runApp() {

    val composer = Recomposer()

    GlobalSnapshotManager.ensureStarted()

    launch(DefaultChoreographerFrameClock) {

    composer.runRecomposeAndApplyChanges()

    }

    Composition(NodeApplier(RootNode()), composer).apply {

    setContent {

    Content()

    }

    }

    }
    @mvndy_hd @jossiwolf

    View Slide

  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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  56. #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

    View Slide

  57. #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

    View Slide

  58. #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

    View Slide

  59. #hg2compose
    private fun readObserverOf(

    composition: ControlledComposition

    ): (Any)
    ->
    Unit {

    return { value
    ->
    composition.recordReadOf(value) }

    }
    Recomposer
    @mvndy_hd @jossiwolf

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  63. @Composable

    fun HelloWorld(greeting: String) {

    Text("Hello, $greeting!")

    }
    #hg2compose
    @mvndy_hd @jossiwolf

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  71. Restartable Groups
    @Composable

    fun HelloWorld(greeting: String) {

    Text("Hello, $greeting!")

    }
    @mvndy_hd @jossiwolf

    View Slide

  72. @Composable

    fun HelloWorld(names: List) {

    names.forEach { name
    ->


    key(name) {

    Text(name)

    }

    }

    }
    Moveable Groups
    @mvndy_hd @jossiwolf

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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 .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

    View Slide

  85. @Composable

    fun HelloWorld() {

    Text("Hello World!")

    }
    #hg2compose
    @mvndy_hd @jossiwolf

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  93. UI
    #hg2compose
    @mvndy_hd @jossiwolf

    View Slide

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

    View Slide

  95. #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()

    }

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  101. #hg2compose
    linktr.ee/hg2compose
    @mvndy_hd @jossiwolf

    View Slide