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

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

    Composition: @Composable → Composition Tree • @Composable updates its node on recomposition @CanyuDev
  3. @Composable Superpowers @CanyuDev

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

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

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

  7. 🧠 Memoization • Values can be retained between compositions •

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

    *.class ▶ 
 Running App Compose UI Compose Runtime @CanyuDev
  9. Compose Compiler Plugin @CanyuDev

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

    • Popular Example • kotlinx.serialization @CanyuDev
  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
  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
  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
  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
  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
  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
  17. Composition Tree / Slot Table @CanyuDev

  18. @CanyuDev

  19. @CanyuDev

  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
  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
  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
  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
  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
  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
  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
  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
  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
  29. Example: Composition Result @CanyuDev

  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 ↩
  31. Recomposition • “State” is observed by runtime 🔍 • “State”

    read attributed “UpdateScope” • Assigned “UpdateScope” is invoked @CanyuDev
  32. “Remember” Operator @Composable fun Application() { val state = remember()

    { "test" } Text(state) } @CanyuDev
  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
  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
  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
  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
  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 ↩
  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 ↩
  39. 💥 What about if/else? • Group ID mismatch detection •

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

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

    tool • Many other applications! @CanyuDev Jetpack Compose internals Jorge Castillo
  42. Questions? @CanyuDev 👋 That’s it folks Can Yumusak