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

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

    View full-size slide

  2. Compose is a UI framework

    View full-size slide

  3. Combination of 7 different libraries

    View full-size slide

  4. • compose.animation


    • compose.foundation


    • compose.material


    • compose.material3


    • compose.ui


    • compose.compiler


    • compose.runtime

    View full-size slide

  5. • compose.animation


    • compose.foundation


    • compose.material


    • compose.material3


    • compose.ui


    • compose.compiler


    • compose.runtime
    🧐

    View full-size slide

  6. Client libraries

    View full-size slide

  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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  10. Agenda
    • Compose Compiler


    • Compose Runtime


    • Composer


    • Slot Table

    View full-size slide

  11. Agenda
    • Applier


    • Client Integration


    • Compose Client Library for PowerPoint

    View full-size slide

  12. Compose Compiler

    View full-size slide

  13. Compose Compiler
    • Kotlin compiler plugin


    • Transforms @Composable functions


    • Targets Compose Runtime

    View full-size slide

  14. @Composable
    Compose


    Compiler Transformed


    code

    View full-size slide

  15. @Composable
    Compose


    Compiler Transformed


    code
    • Injecting composer


    • Stability checks


    • Starting groups


    • Live Literals



    View full-size slide

  16. Compose Runtime 💡

    View full-size slide

  17. Compose Runtime 💡
    Fundamental building blocks of Compose's
    programming model and state management
    https://developer.android.com/jetpack/androidx/releases/compose-runtime

    View full-size slide

  18. Compose Runtime
    • Composer


    • Slot table


    • Applier


    • Compose node tree

    View full-size slide

  19. Composer
    • Targeted by Compose Compiler


    • Bridge between the Composables and Slot Table


    • Insert, update or end groups & nodes


    • Remember values


    • Record the changes

    View full-size slide

  20. @Composable fun Hi() {


    ...


    }
    Compose
    Compiler
    Composable
    called
    Slot Table
    @Composable fun Hi(composer) {


    // Composer starts a group


    ...


    }

    View full-size slide

  21. Slot Table
    A linear & in-memory data structure to keep the
    current state of the composition
    But how?

    View full-size slide

  22. Groups Slots

    View full-size slide

  23. 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

    View full-size slide

  24. https://medium.com/androiddevelopers/under-the-hood-of-jetpack-compose-part-2-of-2-37b2c20c6cdd

    View full-size slide

  25. @Composable fun Hi() {


    ...


    }
    Compose
    Compiler
    Composable
    called
    Slot Table
    @Composable fun Hi(composer) {


    // Composer starts a group


    ...


    }

    View full-size slide

  26. @Composable fun Hi() {


    ...


    }
    Compose
    Compiler
    Composable
    called
    Slot Table
    Composer
    records
    changes
    Applier
    @Composable fun Hi(composer) {


    // Composer starts a group


    ...


    }

    View full-size slide

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

    View full-size slide

  28. 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

    View full-size slide

  29. interface Applier

    View full-size slide

  30. interface Applier
    abstract class AbstractApplier

    View full-size slide

  31. interface Applier
    abstract class AbstractApplier

    View full-size slide

  32. @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


    ...


    }

    View full-size slide

  33. How is the Compose node tree
    created ?

    View full-size slide

  34. @Composable fun Hi() {





    }
    @Composable fun Dev(){





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

    View full-size slide

  35. How is the Compose node
    emitted ?

    View full-size slide

  36. How is the Compose node
    emitted ?
    ReusableComposeNode
    ComposeNode

    View full-size slide

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

    View full-size slide

  38. @Composable fun Hi() {





    }
    @Composable fun Dev(){





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

    View full-size slide

  39. Emits a
    node
    Emits a
    node
    Create or
    update the
    tree
    Applier
    @Composable fun Hi() {


    ComposeNode(…)


    }
    TextNode
    @Composable fun Dev() {


    ComposeNode(…)


    }
    Compose Node Tree
    TextNode

    View full-size slide

  40. @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


    ...


    }

    View full-size slide

  41. Client Integration

    View full-size slide

  42. 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

    View full-size slide

  43. Compose Client Library for
    PowerPoint

    View full-size slide

  44. Compose Client Library for PowerPoint

    View full-size slide

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


    • A custom Applier


    • Composables for the nodes


    • A client integration

    View full-size slide

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


    • A custom Applier


    • Composables for the nodes


    • A client integration

    View full-size slide

  47. abstract class ComposePPTNode {


    /**


    * The children of this node.


    */


    val children = mutableListOf()


    /**


    * Renders the current node.


    */


    abstract fun render()


    }
    Nodes (Base)
    ComposePPTNode.kt

    View full-size slide

  48. abstract class ComposePPTNode {


    /**


    * The children of this node.


    */


    val children = mutableListOf()


    /**


    * Renders the current node.


    */


    abstract fun render()


    }
    Nodes (Base)
    ComposePPTNode.kt

    View full-size slide

  49. abstract class ComposePPTNode {


    /**


    * The children of this node.


    */


    val children = mutableListOf()


    /**


    * Renders the current node.


    */


    abstract fun render()


    }
    Nodes (Base)
    ComposePPTNode.kt

    View full-size slide

  50. class SlideNode : ComposePPTNode() {


    override fun render() {


    children.forEach {


    it.render()


    }


    }


    }
    Nodes (Slide)
    SlideNode.kt

    View full-size slide

  51. class SlideNode : ComposePPTNode() {


    override fun render() {


    children.forEach {


    it.render()


    }


    }


    }
    Nodes (Slide)
    SlideNode.kt

    View full-size slide

  52. class TextNode : ComposePPTNode() {


    var text: String = ""


    override fun render() {


    // Render the text with Apache POI


    createSlideShowAndRenderText(text)


    }


    }
    Nodes (Text)
    TextNode.kt

    View full-size slide

  53. class TextNode : ComposePPTNode() {


    var text: String = ""


    override fun render() {


    // Render the text with Apache POI


    createSlideShowAndRenderText(text)


    }


    }
    Nodes (Text)
    TextNode.kt

    View full-size slide

  54. class TextNode : ComposePPTNode() {


    var text: String = ""


    override fun render() {


    // Render the text with Apache POI


    createSlideShowAndRenderText(text)


    }


    }
    Nodes (Text)
    TextNode.kt

    View full-size slide

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


    • A custom Applier


    • Composables for the nodes


    • A client integration

    View full-size slide

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


    • A custom Applier


    • Composables for the nodes


    • A client integration

    View full-size slide

  57. class ComposePPTApplier(


    root: ComposePPTNode


    ) : AbstractApplier(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

    View full-size slide

  58. class ComposePPTApplier(


    root: ComposePPTNode


    ) : AbstractApplier(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

    View full-size slide

  59. class ComposePPTApplier(


    root: ComposePPTNode


    ) : AbstractApplier(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

    View full-size slide

  60. 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

    View full-size slide

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


    • A custom Applier


    • Composables for the nodes


    • A client integration

    View full-size slide

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


    • A custom Applier


    • Composables for the nodes


    • A client integration

    View full-size slide

  63. @Composable


    fun Slide(content: @Composable () -> Unit) {


    ComposeNode(


    factory = ::SlideNode,


    update = {},


    content = content


    )


    }
    Composables (Slide)
    Slide.kt

    View full-size slide

  64. @Composable


    fun Slide(content: @Composable () -> Unit) {


    ComposeNode(


    factory = ::SlideNode,


    update = {},


    content = content


    )


    }
    Composables (Slide)
    Slide.kt

    View full-size slide

  65. @Composable


    fun Slide(content: @Composable () -> Unit) {


    ComposeNode(


    factory = ::SlideNode,


    update = {},


    content = content


    )


    }
    Composables (Slide)
    Slide.kt

    View full-size slide

  66. @Composable


    fun Slide(content: @Composable () -> Unit) {


    ComposeNode(


    factory = ::SlideNode,


    update = {},


    content = content


    )


    }
    Composables (Slide)
    Slide.kt

    View full-size slide

  67. @Composable


    fun Text(text: String) {


    ComposeNode(


    factory = ::TextNode


    ) {


    set(text) { this.text = it }


    }


    }
    Composables (Text)
    Text.kt

    View full-size slide

  68. @Composable


    fun Text(text: String) {


    ComposeNode(


    factory = ::TextNode


    ) {


    set(text) { this.text = it }


    }


    }
    Composables (Text)
    Text.kt

    View full-size slide

  69. @Composable


    fun Text(text: String) {


    ComposeNode(


    factory = ::TextNode


    ) {


    set(text) { this.text = it }


    }


    }
    Composables (Text)
    Text.kt

    View full-size slide

  70. @Composable


    fun Text(text: String) {


    ComposeNode(


    factory = ::TextNode


    ) {


    set(text) { this.text = it }


    }


    }
    Composables (Text)
    Text.kt

    View full-size slide

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


    • A custom Applier


    • Composables for the nodes


    • A client integration

    View full-size slide

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


    • A custom Applier


    • Composables for the nodes


    • A client integration

    View full-size slide

  73. fun runComposePPT(


    content: @Composable () -> Unit


    ) = runBlocking {


    // 🛠


    }
    Client Integration

    View full-size slide

  74. fun runComposePPT(


    content: @Composable () -> Unit


    ) = runBlocking {


    val frameClock = BroadcastFrameClock()


    }
    Client Integration

    View full-size slide

  75. fun runComposePPT(


    content: @Composable () -> Unit


    ) = runBlocking {


    val frameClock = BroadcastFrameClock()


    val recomposer = Recomposer(coroutineContext + frameClock)


    }
    Client Integration

    View full-size slide

  76. fun runComposePPT(


    content: @Composable () -> Unit


    ) = runBlocking {


    val frameClock = BroadcastFrameClock()


    val recomposer = Recomposer(coroutineContext + frameClock)


    val rootNode = SlideNode()


    }
    Client Integration

    View full-size slide

  77. 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

    View full-size slide

  78. 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

    View full-size slide



  79. // 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

    View full-size slide



  80. // 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

    View full-size slide



  81. 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

    View full-size slide

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


    • A custom Applier


    • Composables for the nodes


    • A client integration

    View full-size slide

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


    • A custom Applier


    • Composables for the nodes


    • A client integration

    View full-size slide

  84. How to use it?

    View full-size slide

  85. fun main()
    Sample App

    View full-size slide

  86. fun main() {


    runComposePPT {


    // Composable content


    }


    }
    Sample App

    View full-size slide

  87. fun main() {


    runComposePPT {


    Slide {


    Text(text = "Hello stranger 👋")


    }


    }


    }
    Sample App

    View full-size slide

  88. fun main() {


    runComposePPT {


    Slide {


    Text(text = "Hello stranger 👋")


    }


    }


    }
    Sample App

    View full-size slide

  89. https://github.com/fgiris/composePPT

    View full-size slide

  90. 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

    View full-size slide

  91. 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

    View full-size slide

  92. linktr.ee/composeppt

    View full-size slide