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

Explained: Compose Compiler and Runtime

Explained: Compose Compiler and Runtime

In July 2021 Jetpack Compose hit a stable release, and since then, many Android Developers around the globe have been cherishing the new way to write Android UI.

Writing UI with Compose feels magical - especially since Compose Code seems to defy the rules of the Java Virtual Machine. So much so that one might wonder how Google built it in a Java-based environment!

With this talk, we will look under the hood of Compose and demystify the superpowers that @Composeable functions offer. You will leave knowing the tricks of the Compose Compiler, get a hold of how to read Compose Bytecode, and understand its Runtime behavior.

Can Yumusak

July 09, 2022
Tweet

Other Decks in Technology

Transcript

  1. Compose Compiler & Runtime
    @CanyuDev • Can Yumusak

    View Slide

  2. What is a @Composable?
    • @Composable functions are DSLs


    • Composition: @Composable → Composition Tree


    • @Composable updates its node on recomposition
    @CanyuDev

    View Slide

  3. @Composable Superpowers
    @CanyuDev

    View Slide

  4. 🥸 Context aware
    • Only callable from other @Composable


    • @Composable ≡ suspend
    @CanyuDev

    View Slide

  5. 🔄 Restartable Code-Blocks
    Application()
    TopAppBar()
    Counter()
    Text()
    Button()
    @CanyuDev

    View Slide

  6. ⏭ Skippable Code-Blocks
    Application()
    TopAppBar()
    Counter()
    Text()
    Button()
    @CanyuDev

    View Slide

  7. 🧠 Memoization
    • Values can be retained between compositions


    • Skip if unchanged input ⏭


    val result = remember(input) {


    anExpensiveComputation(input)


    }


    @CanyuDev

    View Slide

  8. Kotlin Code Journey
    Kotlin
    Compiler
    *.kt
    *.kt
    *.kt
    *.kt
    *.kt
    *.class


    Running
    App
    Compose
    UI
    Compose
    Runtime
    @CanyuDev

    View Slide

  9. Compose Compiler Plugin
    @CanyuDev

    View Slide

  10. 💡 Kotlin Compiler Plugin
    • Hook into Compiler, modify ByteCode


    • Popular Example


    • kotlinx.serialization
    @CanyuDev

    View Slide

  11. 🔎 Code Generation
    • Disclaimer: Everything is simplified~


    @Composable


    fun Counter() {


    var count by remember {


    mutableStateOf(0)


    }


    Text(


    text = "Count: $count",


    modifier = Modifier.clickable { count
    ++
    },


    )


    }


    @CanyuDev

    View Slide

  12. 🔎 Code Generation
    fun Counter(composer: Composer, changed: Int) {


    composer.startRestartGroup(001)


    if (changed
    =
    =
    0
    &
    &
    composer.getSkipping()) {


    composer.skipToGroupEnd()


    } else {


    var count by remember(composer, 0) {


    mutableStateOf(0)


    }


    Text(


    composer,


    0,


    "Count: $count”,


    Modifier.clickable { count
    +
    +
    },


    )


    }


    composer.endRestartGroup()
    ?
    .
    let {


    it.updateScope { composer, force
    ->

    Counter(composer, changed or 1)


    }


    }


    }


    @CanyuDev

    View Slide

  13. 🔎 Code Generation: Passing Context
    fun Counter(composer: Composer, changed: Int) {


    composer.startRestartGroup(001)


    if (changed
    =
    =
    0
    &
    &
    composer.getSkipping()) {


    composer.skipToGroupEnd()


    } else {


    var count by remember(composer, 0) {


    mutableStateOf(0)


    }


    Text(


    composer,


    0,


    "Count: $count”,


    Modifier.clickable { count
    +
    +
    },


    )


    }


    composer.endRestartGroup()
    ?
    .
    let {


    it.updateScope { composer, force
    ->

    Counter(composer, changed or 1)


    }


    }


    }


    @CanyuDev

    View Slide

  14. 🔎 Code Generation: Skipping
    fun Counter(composer: Composer, changed: Int) {


    composer.startRestartGroup(001)


    if (changed
    =
    =
    0
    &
    &
    composer.getSkipping()) {


    composer.skipToGroupEnd()


    } else {


    var count by remember(composer, 0) {


    mutableStateOf(0)


    }


    Text(


    composer,


    0,


    "Count: $count”,


    Modifier.clickable { count
    +
    +
    },


    )


    }


    composer.endRestartGroup()
    ?
    .
    let {


    it.updateScope { composer, force
    ->

    Counter(composer, changed or 1)


    }


    }


    }


    @CanyuDev

    View Slide

  15. 🔎 Code Generation: Restarting
    fun Counter(composer: Composer, changed: Int) {


    composer.startRestartGroup(001)


    if (changed
    =
    =
    0
    &
    &
    composer.getSkipping()) {


    composer.skipToGroupEnd()


    } else {


    var count by remember(composer, 0) {


    mutableStateOf(0)


    }


    Text(


    composer,


    0,


    "Count: $count”,


    Modifier.clickable { count
    +
    +
    },


    )


    }


    composer.endRestartGroup()
    ?
    .
    let {


    it.updateScope { composer, force
    ->

    Counter(composer, changed or 1)


    }


    }


    }


    @CanyuDev

    View Slide

  16. 🔎 Code Generation: Restarting
    fun Counter(composer: Composer, changed: Int) {


    composer.startRestartGroup(001)


    if (changed
    =
    =
    0
    &
    &
    composer.getSkipping()) {


    composer.skipToGroupEnd()


    } else {


    var count by remember(composer, 0) {


    mutableStateOf(0)


    }


    Text(


    composer,


    0,


    "Count: $count”,


    Modifier.clickable { count
    +
    +
    },


    )


    }


    composer.endRestartGroup()
    ?
    .
    let {


    it.updateScope { composer, force
    ->

    Counter(composer, changed or 1)


    }


    }


    }


    @CanyuDev

    View Slide

  17. Composition Tree /


    Slot Table
    @CanyuDev

    View Slide

  18. @CanyuDev

    View Slide

  19. @CanyuDev

    View Slide

  20. Example
    fun Counter(composer: Composer, changed: Int) {


    composer.startRestartGroup(001)


    var count by remember(composer, 0) {


    mutableStateOf(0)


    }


    Text(


    composer,


    0,


    "Count: $count",


    Modifier.clickable { count
    + +
    },


    )


    composer.endRestartGroup()


    }


    @CanyuDev

    View Slide

  21. Example: Composition
    fun Counter(composer: Composer, changed: Int) {


    composer.startRestartGroup(001)


    var count by remember(composer, 0) {


    mutableStateOf(0)


    }


    Text(


    composer,


    0,


    "Count: $count",


    Modifier.clickable { count
    + +
    },


    )


    composer.endRestartGroup()


    }


    @CanyuDev

    View Slide

  22. Example: Composition
    fun Counter(composer: Composer, changed: Int) {


    composer.startRestartGroup(001)


    var count by remember(composer, 0) {


    mutableStateOf(0)


    }


    Text(


    composer,


    0,


    "Count: $count",


    Modifier.clickable { count
    + +
    },


    )


    composer.endRestartGroup()


    }


    @CanyuDev

    View Slide

  23. Example: Composition
    fun Counter(composer: Composer, changed: Int) {


    composer.startRestartGroup(001)


    var count by remember(composer, 0) {


    mutableStateOf(0)


    }


    Text(


    composer,


    0,


    "Count: $count",


    Modifier.clickable { count
    + +
    },


    )


    composer.endRestartGroup()


    }


    @CanyuDev

    View Slide

  24. Example: Composition
    fun Counter(composer: Composer, changed: Int) {


    composer.startRestartGroup(001)


    var count by remember(composer, 0) {


    mutableStateOf(0)


    }


    Text(


    composer,


    0,


    "Count: $count",


    Modifier.clickable { count
    + +
    },


    )


    composer.endRestartGroup()


    }


    @CanyuDev

    View Slide

  25. Example: Composition
    fun Counter(composer: Composer, changed: Int) {


    composer.startRestartGroup(001)


    var count by remember(composer, 0) {


    mutableStateOf(0)


    }


    Text(


    composer,


    0,


    "Count: $count",


    Modifier.clickable { count
    + +
    },


    )


    composer.endRestartGroup()


    }


    @CanyuDev

    View Slide

  26. Example: Composition
    fun Counter(composer: Composer, changed: Int) {


    composer.startRestartGroup(001)


    var count by remember(composer, 0) {


    mutableStateOf(0)


    }


    Text(


    composer,


    0,


    "Count: $count",


    Modifier.clickable { count
    + +
    },


    )


    composer.endRestartGroup()


    }


    @CanyuDev

    View Slide

  27. Example: Composition
    fun Counter(composer: Composer, changed: Int) {


    composer.startRestartGroup(001)


    var count by remember(composer, 0) {


    mutableStateOf(0)


    }


    Text(


    composer,


    0,


    "Count: $count",


    Modifier.clickable { count
    + +
    },


    )


    composer.endRestartGroup()


    }


    @CanyuDev

    View Slide

  28. Example: Composition
    fun Counter(composer: Composer, changed: Int) {


    composer.startRestartGroup(001)


    var count by remember(composer, 0) {


    mutableStateOf(0)


    }


    Text(


    composer,


    0,


    "Count: $count",


    Modifier.clickable { count
    + +
    },


    )


    composer.endRestartGroup()


    }


    @CanyuDev

    View Slide

  29. Example: Composition Result
    @CanyuDev

    View Slide

  30. Example: Recompostion
    fun Counter(composer: Composer, changed: Int) {


    composer.startRestartGroup(001)


    var count by remember(composer, 0) {


    mutableStateOf(0)


    }


    Text(


    composer,


    0,


    "Count: $count",


    Modifier.clickable { count
    + +
    },


    )


    composer.endRestartGroup()


    }


    @CanyuDev

    View Slide

  31. Recomposition
    • “State” is observed by runtime 🔍


    • “State” read attributed “UpdateScope”


    • Assigned “UpdateScope” is invoked
    @CanyuDev

    View Slide

  32. “Remember” Operator
    @Composable


    fun Application() {


    val state = remember() { "test" }


    Text(state)


    }


    @CanyuDev

    View Slide

  33. “Remember” Operator
    fun Application(…) {


    val remembered = composer.rememberedValue()


    val value: String


    if (remembered
    !=
    Composer.Companion.Empty) {


    value = remembered as String


    } else {


    val newValue = “test"
    //
    { "test" }


    composer.updateRememberedValue(newValue)


    value = newValue


    }


    Text(value)


    }


    @CanyuDev

    View Slide

  34. “Remember” Operator
    fun Application(…) {


    val remembered = composer.rememberedValue()


    val value: String


    if (remembered
    !=
    Composer.Companion.Empty) {


    value = remembered as String


    } else {


    val newValue = “test"
    //
    { "test" }


    composer.updateRememberedValue(newValue)


    value = newValue


    }


    Text(value)


    }


    @CanyuDev

    View Slide

  35. “Remember” Operator
    fun Application(…) {


    val remembered = composer.rememberedValue()


    val value: String


    if (remembered
    !=
    Composer.Companion.Empty) {


    value = remembered as String


    } else {


    val newValue = “test"
    //
    { "test" }


    composer.updateRememberedValue(newValue)


    value = newValue


    }


    Text(value)


    }


    @CanyuDev

    View Slide

  36. “Remember” Operator
    fun Application(…) {


    val remembered = composer.rememberedValue()


    val value: String


    if (remembered
    !=
    Composer.Companion.Empty) {


    value = remembered as String


    } else {


    val newValue = “test”
    //
    { "test" }


    composer.updateRememberedValue(newValue)


    value = newValue


    }


    Text(value)


    }


    @CanyuDev

    View Slide

  37. “Remember” Operator
    fun Application(…) {


    val remembered = composer.rememberedValue()


    val value: String


    if (remembered
    !=
    Composer.Companion.Empty) {


    value = remembered as String


    } else {


    val newValue = “test"
    //
    { "test" }


    composer.updateRememberedValue(newValue)


    value = newValue


    }


    Text(value)


    }


    @CanyuDev

    View Slide

  38. “Remember” Operator
    fun Application(…) {


    val remembered = composer.rememberedValue()


    val value: String


    if (remembered
    !=
    Composer.Companion.Empty) {


    value = remembered as String


    } else {


    val newValue = “test"
    //
    { "test" }


    composer.updateRememberedValue(newValue)


    value = newValue


    }


    Text(value)


    }


    @CanyuDev

    View Slide

  39. 💥 What about if/else?
    • Group ID mismatch detection


    • Solved by deleting group in slot table


    • Expensive but Rare
    @CanyuDev

    View Slide

  40. 🔑 Key Takeaways
    • Compiler Plugin generates Code


    • Generated code reads / writes <> Slot Table


    • Skip known code parts


    • Only emit new changes
    @CanyuDev

    View Slide

  41. 🧐 Summary
    • Compose Runtime is a very powerful diffing tool


    • Many other applications!
    @CanyuDev
    Jetpack Compose internals


    Jorge Castillo

    View Slide

  42. Questions?
    @CanyuDev
    👋 That’s it folks
    Can Yumusak

    View Slide