Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

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. Compose Runtime 💡 Fundamental building blocks of Compose's programming model

    and state management https://developer.android.com/jetpack/androidx/releases/compose-runtime
  2. Composer • Targeted by Compose Compiler • Bridge between the

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

    Slot Table @Composable fun Hi(composer) { // Composer starts a group ... }
  4. Slot Table A linear & in-memory data structure to keep

    the current state of the composition But how?
  5. 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
  6. @Composable fun Hi() { ... } Compose Compiler Composable called

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

    Slot Table Composer records changes Applier @Composable fun Hi(composer) { // Composer starts a group ... }
  8. Applier Responsible for applying the tree-based operations that get emitted

    during a composition* https://developer.android.com/reference/kotlin/androidx/compose/runtime/Applier
  9. 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
  10. @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 ... }
  11. @Composable fun Hi() { … } @Composable fun Dev(){ …

    } Emits a node Emits a node Create or update the tree Applier Compose Node Tree
  12. @Composable fun Hi() { … } @Composable fun Dev(){ …

    } Emits a node Emits a node Create or update the tree Applier Compose Node Tree
  13. 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
  14. @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 ... }
  15. 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
  16. Compose Client Library for PowerPoint • Nodes (Text & Slide)

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

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

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

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

    node. */ val children = mutableListOf<ComposePPTNode>() /** * Renders the current node. */ abstract fun render() } Nodes (Base) ComposePPTNode.kt
  21. class TextNode : ComposePPTNode() { var text: String = ""

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

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

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

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

    • A custom Applier • Composables for the nodes • A client integration
  26. 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
  27. 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
  28. 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
  29. 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
  30. Compose Client Library for PowerPoint • Nodes (Text & Slide)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    { val frameClock = BroadcastFrameClock() val recomposer = Recomposer(coroutineContext + frameClock) val rootNode = SlideNode() } Client Integration
  45. 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
  46. 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
  47. // 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
  48. // 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
  49. 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
  50. Compose Client Library for PowerPoint • Nodes (Text & Slide)

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

    • A custom Applier • Composables for the nodes • A client integration
  52. 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
  53. 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