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 Slide

  2. Compose is a UI framework

    View Slide

  3. Combination of 7 different libraries

    View Slide

  4. • compose.animation


    • compose.foundation


    • compose.material


    • compose.material3


    • compose.ui


    • compose.compiler


    • compose.runtime

    View Slide

  5. • compose.animation


    • compose.foundation


    • compose.material


    • compose.material3


    • compose.ui


    • compose.compiler


    • compose.runtime
    🧐

    View Slide

  6. Client libraries

    View 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 Slide

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

    View Slide

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

    View Slide

  10. Agenda
    • Compose Compiler


    • Compose Runtime


    • Composer


    • Slot Table

    View Slide

  11. Agenda
    • Applier


    • Client Integration


    • Compose Client Library for PowerPoint

    View Slide

  12. Compose Compiler

    View Slide

  13. Compose Compiler
    • Kotlin compiler plugin


    • Transforms @Composable functions


    • Targets Compose Runtime

    View Slide

  14. @Composable
    Compose


    Compiler Transformed


    code

    View Slide

  15. @Composable
    Compose


    Compiler Transformed


    code
    • Injecting composer


    • Stability checks


    • Starting groups


    • Live Literals



    View Slide

  16. Compose Runtime 💡

    View 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 Slide

  18. Compose Runtime
    • Composer


    • Slot table


    • Applier


    • Compose node tree

    View Slide

  19. Composer

    View Slide

  20. Composer
    • Targeted by Compose Compiler


    • Bridge between the Composables and Slot Table


    • Insert, update or end groups & nodes


    • Remember values


    • Record the changes

    View Slide

  21. @Composable fun Hi() {


    ...


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


    // Composer starts a group


    ...


    }

    View Slide

  22. Slot Table

    View Slide

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

    View Slide

  24. Groups Slots

    View Slide

  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

    View Slide

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

    View Slide

  27. @Composable fun Hi() {


    ...


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


    // Composer starts a group


    ...


    }

    View Slide

  28. @Composable fun Hi() {


    ...


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


    // Composer starts a group


    ...


    }

    View Slide

  29. Applier

    View Slide

  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

    View Slide

  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

    View Slide

  32. interface Applier

    View Slide

  33. interface Applier
    abstract class AbstractApplier

    View Slide

  34. interface Applier
    abstract class AbstractApplier

    View Slide

  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


    ...


    }

    View Slide

  36. How is the Compose node tree
    created ?

    View Slide

  37. @Composable fun Hi() {





    }
    @Composable fun Dev(){





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

    View Slide

  38. How is the Compose node
    emitted ?

    View Slide

  39. How is the Compose node
    emitted ?
    ReusableComposeNode
    ComposeNode

    View Slide

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

    View Slide

  41. @Composable fun Hi() {





    }
    @Composable fun Dev(){





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

    View Slide

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

  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


    ...


    }

    View Slide

  44. Client Integration

    View Slide

  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

    View Slide

  46. Compose Client Library for
    PowerPoint

    View Slide

  47. Compose Client Library for PowerPoint

    View Slide

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


    • A custom Applier


    • Composables for the nodes


    • A client integration

    View Slide

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


    • A custom Applier


    • Composables for the nodes


    • A client integration

    View Slide

  50. abstract class ComposePPTNode {


    /**


    * The children of this node.


    */


    val children = mutableListOf()


    /**


    * Renders the current node.


    */


    abstract fun render()


    }
    Nodes (Base)
    ComposePPTNode.kt

    View Slide

  51. abstract class ComposePPTNode {


    /**


    * The children of this node.


    */


    val children = mutableListOf()


    /**


    * Renders the current node.


    */


    abstract fun render()


    }
    Nodes (Base)
    ComposePPTNode.kt

    View Slide

  52. abstract class ComposePPTNode {


    /**


    * The children of this node.


    */


    val children = mutableListOf()


    /**


    * Renders the current node.


    */


    abstract fun render()


    }
    Nodes (Base)
    ComposePPTNode.kt

    View Slide

  53. class SlideNode : ComposePPTNode() {


    override fun render() {


    children.forEach {


    it.render()


    }


    }


    }
    Nodes (Slide)
    SlideNode.kt

    View Slide

  54. class SlideNode : ComposePPTNode() {


    override fun render() {


    children.forEach {


    it.render()


    }


    }


    }
    Nodes (Slide)
    SlideNode.kt

    View Slide

  55. class TextNode : ComposePPTNode() {


    var text: String = ""


    override fun render() {


    // Render the text with Apache POI


    createSlideShowAndRenderText(text)


    }


    }
    Nodes (Text)
    TextNode.kt

    View Slide

  56. class TextNode : ComposePPTNode() {


    var text: String = ""


    override fun render() {


    // Render the text with Apache POI


    createSlideShowAndRenderText(text)


    }


    }
    Nodes (Text)
    TextNode.kt

    View Slide

  57. class TextNode : ComposePPTNode() {


    var text: String = ""


    override fun render() {


    // Render the text with Apache POI


    createSlideShowAndRenderText(text)


    }


    }
    Nodes (Text)
    TextNode.kt

    View Slide

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


    • A custom Applier


    • Composables for the nodes


    • A client integration

    View Slide

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


    • A custom Applier


    • Composables for the nodes


    • A client integration

    View Slide

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

  61. 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 Slide

  62. 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 Slide

  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

    View Slide

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


    • A custom Applier


    • Composables for the nodes


    • A client integration

    View Slide

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


    • A custom Applier


    • Composables for the nodes


    • A client integration

    View Slide

  66. @Composable


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


    ComposeNode(


    factory = ::SlideNode,


    update = {},


    content = content


    )


    }
    Composables (Slide)
    Slide.kt

    View Slide

  67. @Composable


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


    ComposeNode(


    factory = ::SlideNode,


    update = {},


    content = content


    )


    }
    Composables (Slide)
    Slide.kt

    View Slide

  68. @Composable


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


    ComposeNode(


    factory = ::SlideNode,


    update = {},


    content = content


    )


    }
    Composables (Slide)
    Slide.kt

    View Slide

  69. @Composable


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


    ComposeNode(


    factory = ::SlideNode,


    update = {},


    content = content


    )


    }
    Composables (Slide)
    Slide.kt

    View Slide

  70. @Composable


    fun Text(text: String) {


    ComposeNode(


    factory = ::TextNode


    ) {


    set(text) { this.text = it }


    }


    }
    Composables (Text)
    Text.kt

    View Slide

  71. @Composable


    fun Text(text: String) {


    ComposeNode(


    factory = ::TextNode


    ) {


    set(text) { this.text = it }


    }


    }
    Composables (Text)
    Text.kt

    View Slide

  72. @Composable


    fun Text(text: String) {


    ComposeNode(


    factory = ::TextNode


    ) {


    set(text) { this.text = it }


    }


    }
    Composables (Text)
    Text.kt

    View Slide

  73. @Composable


    fun Text(text: String) {


    ComposeNode(


    factory = ::TextNode


    ) {


    set(text) { this.text = it }


    }


    }
    Composables (Text)
    Text.kt

    View Slide

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


    • A custom Applier


    • Composables for the nodes


    • A client integration

    View Slide

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


    • A custom Applier


    • Composables for the nodes


    • A client integration

    View Slide

  76. fun runComposePPT(


    content: @Composable () -> Unit


    ) = runBlocking {


    // 🛠


    }
    Client Integration

    View Slide

  77. fun runComposePPT(


    content: @Composable () -> Unit


    ) = runBlocking {


    val frameClock = BroadcastFrameClock()


    }
    Client Integration

    View Slide

  78. fun runComposePPT(


    content: @Composable () -> Unit


    ) = runBlocking {


    val frameClock = BroadcastFrameClock()


    val recomposer = Recomposer(coroutineContext + frameClock)


    }
    Client Integration

    View Slide

  79. fun runComposePPT(


    content: @Composable () -> Unit


    ) = runBlocking {


    val frameClock = BroadcastFrameClock()


    val recomposer = Recomposer(coroutineContext + frameClock)


    val rootNode = SlideNode()


    }
    Client Integration

    View Slide

  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

    View Slide

  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

    View Slide



  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

    View Slide



  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

    View Slide



  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

    View Slide

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


    • A custom Applier


    • Composables for the nodes


    • A client integration

    View Slide

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


    • A custom Applier


    • Composables for the nodes


    • A client integration

    View Slide

  87. How to use it?

    View Slide

  88. fun main()
    Sample App

    View Slide

  89. fun main() {


    runComposePPT {


    // Composable content


    }


    }
    Sample App

    View Slide

  90. fun main() {


    runComposePPT {


    Slide {


    Text(text = "Hello stranger 👋")


    }


    }


    }
    Sample App

    View Slide

  91. fun main() {


    runComposePPT {


    Slide {


    Text(text = "Hello stranger 👋")


    }


    }


    }
    Sample App

    View Slide

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

    View Slide

  93. TL;DR

    View Slide

  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

    View Slide

  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

    View Slide

  96. Thanks!

    View Slide

  97. linktr.ee/composeppt

    View Slide