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. What is a @Composable? • @Composable functions are DSLs •

    Composition: @Composable → Composition Tree • @Composable updates its node on recomposition @CanyuDev
  2. 🧠 Memoization • Values can be retained between compositions •

    Skip if unchanged input ⏭ val result = remember(input) { anExpensiveComputation(input) } @CanyuDev
  3. Kotlin Code Journey Kotlin Compiler *.kt *.kt *.kt *.kt *.kt

    *.class ▶ 
 Running App Compose UI Compose Runtime @CanyuDev
  4. 💡 Kotlin Compiler Plugin • Hook into Compiler, modify ByteCode

    • Popular Example • kotlinx.serialization @CanyuDev
  5. 🔎 Code Generation • Disclaimer: Everything is simplified~ @Composable fun

    Counter() { var count by remember { mutableStateOf(0) } Text( text = "Count: $count", modifier = Modifier.clickable { count ++ }, ) } @CanyuDev
  6. 🔎 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
  7. 🔎 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
  8. 🔎 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
  9. 🔎 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
  10. 🔎 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
  11. 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
  12. 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
  13. 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
  14. 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
  15. 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
  16. 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
  17. 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
  18. 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
  19. 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
  20. 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 ↩
  21. Recomposition • “State” is observed by runtime 🔍 • “State”

    read attributed “UpdateScope” • Assigned “UpdateScope” is invoked @CanyuDev
  22. “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
  23. “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
  24. “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
  25. “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
  26. “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 ↩
  27. “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 ↩
  28. 💥 What about if/else? • Group ID mismatch detection •

    Solved by deleting group in slot table • Expensive but Rare @CanyuDev
  29. 🔑 Key Takeaways • Compiler Plugin generates Code • Generated

    code reads / writes <> Slot Table • Skip known code parts • Only emit new changes @CanyuDev
  30. 🧐 Summary • Compose Runtime is a very powerful diffing

    tool • Many other applications! @CanyuDev Jetpack Compose internals Jorge Castillo