$30 off During Our Annual Pro Sale. View details »

Using Compose Runtime to create a client library

Using Compose Runtime to create a client library

Jetpack Compose (UI) is a powerful UI toolkit for Android. Have you ever wondered where this power comes from? The answer is Compose Runtime.

In this talk, we will see how we can use Compose Runtime to create client libraries. Firstly, we will talk about Compose nodes, Composition, Recomposer, and how they are orchestrated to create a slot table. Then, we will see how the changes in the slot table are applied with an Applier. Moreover, we will touch upon the Snapshot system and how the changes in the state objects trigger a recomposition. Finally, we will create a basic UI toolkit for PowerPoint using Compose Runtime.

Fatih Giriş

July 09, 2022
Tweet

More Decks by Fatih Giriş

Other Decks in Technology

Transcript

  1. Using Compose Runtime to Create a Client Library Fatih Giris

    Android Lead @DNB Bank
  2. Compose is a UI framework

  3. Combination of 7 different libraries

  4. • compose.animation • compose.foundation • compose.material • compose.material3 • compose.ui

    • compose.compiler • compose.runtime
  5. • compose.animation • compose.foundation • compose.material • compose.material3 • compose.ui

    • compose.compiler • compose.runtime 🧐
  6. Client libraries

  7. Client libraries APIs for use in writing client applications* *https://infocenter.sybase.com/help/index.jsp?topic=/com.sybase.infocenter.dc36065.1570/html/ctlibmig/X55077.htm

  8. https://developer.android.com/jetpack/compose/layering

  9. https://developer.android.com/jetpack/compose/layering Client Libraries

  10. Agenda • Compose Compiler • Compose Runtime • Composer •

    Slot Table
  11. Agenda • Applier • Client Integration • Compose Client Library

    for PowerPoint
  12. Compose Compiler

  13. Compose Compiler • Kotlin compiler plugin • Transforms @Composable functions

    • Targets Compose Runtime
  14. @Composable Compose Compiler Transformed code

  15. @Composable Compose Compiler Transformed code • Injecting composer • Stability

    checks • Starting groups • Live Literals …
  16. Compose Runtime 💡

  17. Compose Runtime 💡 Fundamental building blocks of Compose's programming model

    and state management https://developer.android.com/jetpack/androidx/releases/compose-runtime
  18. Compose Runtime • Composer • Slot table • Applier •

    Compose node tree
  19. Composer

  20. Composer • Targeted by Compose Compiler • Bridge between the

    Composables and Slot Table • Insert, update or end groups & nodes • Remember values • Record the changes
  21. @Composable fun Hi() { ... } Compose Compiler Composable called

    Slot Table @Composable fun Hi(composer) { // Composer starts a group ... }
  22. Slot Table

  23. Slot Table A linear & in-memory data structure to keep

    the current state of the composition But how?
  24. Groups Slots

  25. Groups Slots Turns the linear cache into a tree- like

    structure* *http://intelligiblebabble.com/compose-from- fi rst-principles Keeps the actual data for the groups
  26. https://medium.com/androiddevelopers/under-the-hood-of-jetpack-compose-part-2-of-2-37b2c20c6cdd

  27. @Composable fun Hi() { ... } Compose Compiler Composable called

    Slot Table @Composable fun Hi(composer) { // Composer starts a group ... }
  28. @Composable fun Hi() { ... } Compose Compiler Composable called

    Slot Table Composer records changes Applier @Composable fun Hi(composer) { // Composer starts a group ... }
  29. Applier

  30. Applier Responsible for applying the tree-based operations that get emitted

    during a composition* https://developer.android.com/reference/kotlin/androidx/compose/runtime/Applier
  31. Applier Responsible for applying the tree-based operations that get emitted

    during a composition* https://developer.android.com/reference/kotlin/androidx/compose/runtime/Applier Remove Move Insert
  32. interface Applier

  33. interface Applier abstract class AbstractApplier

  34. interface Applier abstract class AbstractApplier

  35. @Composable fun Hi() { ... } Compose Compiler Composable called

    Slot Table Composer records changes Applier Changes applied Compose Node Tree @Composable fun Hi(composer) { // Composer starts a group ... }
  36. How is the Compose node tree created ?

  37. @Composable fun Hi() { … } @Composable fun Dev(){ …

    } Emits a node Emits a node Create or update the tree Applier Compose Node Tree
  38. How is the Compose node emitted ?

  39. How is the Compose node emitted ? ReusableComposeNode ComposeNode

  40. https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composables.kt

  41. @Composable fun Hi() { … } @Composable fun Dev(){ …

    } Emits a node Emits a node Create or update the tree Applier Compose Node Tree
  42. Emits a node Emits a node Create or update the

    tree Applier @Composable fun Hi() { ComposeNode<TextNode,…>(…) } TextNode @Composable fun Dev() { ComposeNode<TextNode,…>(…) } Compose Node Tree TextNode
  43. @Composable fun Hi() { ... } Compose Compiler Composable called

    Slot Table Composer records changes Applier Changes applied Compose Node Tree Materialize UI @Composable fun Hi(composer) { // Composer starts a group ... }
  44. Client Integration

  45. Client Integration • Gets the Composable content • Creates a

    frame clock and Recomposer • Creates the initial Composition • Observes and applies changes from the Snapshot objects • Refreshes the frame
  46. Compose Client Library for PowerPoint

  47. Compose Client Library for PowerPoint

  48. Compose Client Library for PowerPoint • Nodes (Text & Slide)

    • A custom Applier • Composables for the nodes • A client integration
  49. Compose Client Library for PowerPoint • Nodes (Text & Slide)

    • A custom Applier • Composables for the nodes • A client integration
  50. abstract class ComposePPTNode { /** * The children of this

    node. */ val children = mutableListOf<ComposePPTNode>() /** * Renders the current node. */ abstract fun render() } Nodes (Base) ComposePPTNode.kt
  51. abstract class ComposePPTNode { /** * The children of this

    node. */ val children = mutableListOf<ComposePPTNode>() /** * Renders the current node. */ abstract fun render() } Nodes (Base) ComposePPTNode.kt
  52. abstract class ComposePPTNode { /** * The children of this

    node. */ val children = mutableListOf<ComposePPTNode>() /** * Renders the current node. */ abstract fun render() } Nodes (Base) ComposePPTNode.kt
  53. class SlideNode : ComposePPTNode() { override fun render() { children.forEach

    { it.render() } } } Nodes (Slide) SlideNode.kt
  54. class SlideNode : ComposePPTNode() { override fun render() { children.forEach

    { it.render() } } } Nodes (Slide) SlideNode.kt
  55. class TextNode : ComposePPTNode() { var text: String = ""

    override fun render() { // Render the text with Apache POI createSlideShowAndRenderText(text) } } Nodes (Text) TextNode.kt
  56. class TextNode : ComposePPTNode() { var text: String = ""

    override fun render() { // Render the text with Apache POI createSlideShowAndRenderText(text) } } Nodes (Text) TextNode.kt
  57. class TextNode : ComposePPTNode() { var text: String = ""

    override fun render() { // Render the text with Apache POI createSlideShowAndRenderText(text) } } Nodes (Text) TextNode.kt
  58. Compose Client Library for PowerPoint • Nodes (Text & Slide)

    • A custom Applier • Composables for the nodes • A client integration
  59. Compose Client Library for PowerPoint • Nodes (Text & Slide)

    • A custom Applier • Composables for the nodes • A client integration
  60. class ComposePPTApplier( root: ComposePPTNode ) : AbstractApplier<ComposePPTNode>(root) { override fun

    insertTopDown(index: Int, instance: ComposePPTNode) { current.children.add(index, instance) } override fun insertBottomUp(index: Int, instance: ComposePPTNode) { // Ignored as the tree is built top-down. } override fun remove(index: Int, count: Int) { current.children.remove(index, count) } override fun move(from: Int, to: Int, count: Int) { current.children.move(from, to, count) } override fun onClear() { root.children.clear() } } Applier ComposePPTApplier.kt
  61. class ComposePPTApplier( root: ComposePPTNode ) : AbstractApplier<ComposePPTNode>(root) { override fun

    insertTopDown(index: Int, instance: ComposePPTNode) { current.children.add(index, instance) } override fun insertBottomUp(index: Int, instance: ComposePPTNode) { // Ignored as the tree is built top-down. } override fun remove(index: Int, count: Int) { current.children.remove(index, count) } override fun move(from: Int, to: Int, count: Int) { current.children.move(from, to, count) Applier
  62. class ComposePPTApplier( root: ComposePPTNode ) : AbstractApplier<ComposePPTNode>(root) { override fun

    insertTopDown(index: Int, instance: ComposePPTNode) { current.children.add(index, instance) } override fun insertBottomUp(index: Int, instance: ComposePPTNode) { // Ignored as the tree is built top-down. } override fun remove(index: Int, count: Int) { current.children.remove(index, count) } override fun move(from: Int, to: Int, count: Int) { current.children.move(from, to, count) Applier
  63. override fun insertTopDown(index: Int, instance: ComposePPTNode) { current.children.add(index, instance) }

    override fun insertBottomUp(index: Int, instance: ComposePPTNode) { // Ignored as the tree is built top-down. } override fun remove(index: Int, count: Int) { current.children.remove(index, count) } override fun move(from: Int, to: Int, count: Int) { current.children.move(from, to, count) } override fun onClear() { root.children.clear() } } Applier
  64. Compose Client Library for PowerPoint • Nodes (Text & Slide)

    • A custom Applier • Composables for the nodes • A client integration
  65. Compose Client Library for PowerPoint • Nodes (Text & Slide)

    • A custom Applier • Composables for the nodes • A client integration
  66. @Composable fun Slide(content: @Composable () -> Unit) { ComposeNode<SlideNode, ComposePPTApplier>(

    factory = ::SlideNode, update = {}, content = content ) } Composables (Slide) Slide.kt
  67. @Composable fun Slide(content: @Composable () -> Unit) { ComposeNode<SlideNode, ComposePPTApplier>(

    factory = ::SlideNode, update = {}, content = content ) } Composables (Slide) Slide.kt
  68. @Composable fun Slide(content: @Composable () -> Unit) { ComposeNode<SlideNode, ComposePPTApplier>(

    factory = ::SlideNode, update = {}, content = content ) } Composables (Slide) Slide.kt
  69. @Composable fun Slide(content: @Composable () -> Unit) { ComposeNode<SlideNode, ComposePPTApplier>(

    factory = ::SlideNode, update = {}, content = content ) } Composables (Slide) Slide.kt
  70. @Composable fun Text(text: String) { ComposeNode<TextNode, ComposePPTApplier>( factory = ::TextNode

    ) { set(text) { this.text = it } } } Composables (Text) Text.kt
  71. @Composable fun Text(text: String) { ComposeNode<TextNode, ComposePPTApplier>( factory = ::TextNode

    ) { set(text) { this.text = it } } } Composables (Text) Text.kt
  72. @Composable fun Text(text: String) { ComposeNode<TextNode, ComposePPTApplier>( factory = ::TextNode

    ) { set(text) { this.text = it } } } Composables (Text) Text.kt
  73. @Composable fun Text(text: String) { ComposeNode<TextNode, ComposePPTApplier>( factory = ::TextNode

    ) { set(text) { this.text = it } } } Composables (Text) Text.kt
  74. Compose Client Library for PowerPoint • Nodes (Text & Slide)

    • A custom Applier • Composables for the nodes • A client integration
  75. Compose Client Library for PowerPoint • Nodes (Text & Slide)

    • A custom Applier • Composables for the nodes • A client integration
  76. fun runComposePPT( content: @Composable () -> Unit ) = runBlocking

    { // 🛠 } Client Integration
  77. fun runComposePPT( content: @Composable () -> Unit ) = runBlocking

    { val frameClock = BroadcastFrameClock() } Client Integration
  78. fun runComposePPT( content: @Composable () -> Unit ) = runBlocking

    { val frameClock = BroadcastFrameClock() val recomposer = Recomposer(coroutineContext + frameClock) } Client Integration
  79. fun runComposePPT( content: @Composable () -> Unit ) = runBlocking

    { val frameClock = BroadcastFrameClock() val recomposer = Recomposer(coroutineContext + frameClock) val rootNode = SlideNode() } Client Integration
  80. fun runComposePPT( content: @Composable () -> Unit ) = runBlocking

    { val frameClock = BroadcastFrameClock() val recomposer = Recomposer(coroutineContext + frameClock) val rootNode = SlideNode() // Register an observer for any snapshot object change Snapshot.registerGlobalWriteObserver { // Whenever any snapshot object changes, send an apply noti fi cation // so that Recomposer can trigger the recomposition Snapshot.sendApplyNotifications() } } Client Integration
  81. content: @Composable () -> Unit ) = runBlocking { val

    frameClock = BroadcastFrameClock() val recomposer = Recomposer(coroutineContext + frameClock) val rootNode = SlideNode() // Register an observer for any snapshot object change Snapshot.registerGlobalWriteObserver { // Whenever any snapshot object changes, send an apply noti fi cation // so that Recomposer can trigger the recomposition Snapshot.sendApplyNotifications() } // Create the initial composition val composition = Composition( applier = ComposePPTApplier(rootNode), parent = recomposer ) } Client Integration
  82. // Register an observer for any snapshot object change Snapshot.registerGlobalWriteObserver

    { // Whenever any snapshot object changes, send an apply noti fi cation // so that Recomposer can trigger the recomposition Snapshot.sendApplyNotifications() } // Create the initial composition val composition = Composition( applier = ComposePPTApplier(rootNode), parent = recomposer ) // Need to trigger invoking the composable content composition.setContent(content) } Client Integration
  83. // Create the initial composition val composition = Composition( applier

    = ComposePPTApplier(rootNode), parent = recomposer ) // Need to trigger invoking the composable content composition.setContent(content) // Starting undispatched in order not to send a frame before // the Recomposer registers an awaiter. launch(context = frameClock, start = CoroutineStart.UNDISPATCHED) { recomposer.runRecomposeAndApplyChanges() } } Client Integration
  84. composition.setContent(content) // Starting undispatched in order not to send a

    frame before // the Recomposer registers an awaiter. launch(context = frameClock, start = CoroutineStart.UNDISPATCHED) { recomposer.runRecomposeAndApplyChanges() } launch { // Sends a frame every 100 ms & render it while (true) { frameClock.sendFrame(System.nanoTime()) rootNode.render() // Refresh rate for each frame delay(100L) } } } Client Integration
  85. Compose Client Library for PowerPoint • Nodes (Text & Slide)

    • A custom Applier • Composables for the nodes • A client integration
  86. Compose Client Library for PowerPoint • Nodes (Text & Slide)

    • A custom Applier • Composables for the nodes • A client integration
  87. How to use it?

  88. fun main() Sample App

  89. fun main() { runComposePPT { // Composable content } }

    Sample App
  90. fun main() { runComposePPT { Slide { Text(text = "Hello

    stranger 👋") } } } Sample App
  91. fun main() { runComposePPT { Slide { Text(text = "Hello

    stranger 👋") } } } Sample App
  92. https://github.com/fgiris/composePPT

  93. TL;DR

  94. TL;DR • Compose Compiler transforms @Composable functions • Composer is

    the bridge between the Composables and the Slot Table • Slot Table keeps the current state of the composition • Applier is responsible for the tree-based operations for the emitted node
  95. TL;DR • Client integration • Creating the initial composition •

    Getting & setting the composable content • Listening state object changes and sending apply noti fi cations • Waiting for any recomposition • Refreshing the output
  96. Thanks!

  97. linktr.ee/composeppt