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

Shifting Paradigms

Shifting Paradigms

Droidjam 2019, Radisson Blu, Bengaluru

Reactive applications are the norm on the web. Android applications have begun increasingly reactive using architectures like MVVM, MVI, Redux, etc., The reactive nature of these applications are due to the underlying architecture rather than the UI framework itself. This has resulted in some big wins like testability and strong separation of concerns while trading-off attributes like time-to-market and quickly on-boarding new developers to an existing project.

The API surface of the Android UI framework has been the same for the past decade. This was largely because of the limitations imposed by old tooling and the programming language. With Kotlin bringing in DSLs, functional programming and compiler plugin capabilities - the members of the Android UI Toolkit team have rolled up their sleeves and are taking a new direction. The new UI toolkit is backed by principles and techniques like - one-way data flow, function composition, declarative programming, optimistic concurrency control, isolation of side-effects, etc.,

This shift in API design also demands a shift in thinking about the app architecture and building UIs. In this talk, we’ll see how Jetpack Compose is radically different from the existing Android UI framework and how it adopts several ideas and programming patterns from web frameworks like React, MobX, and Vue.js. We will also see examples of these patterns and use them to build Android apps.

Ragunath Jawahar

August 30, 2019
Tweet

More Decks by Ragunath Jawahar

Other Decks in Programming

Transcript

  1. Shifting Paradigms
    Ragunath Jawahar / @ragunathjawahar

    View Slide

  2. One moment…

    View Slide

  3. View Slide

  4. View Slide

  5. View Slide

  6. View Slide

  7. View Slide

  8. View Slide

  9. View Slide

  10. View Slide

  11. Hiccups
    • A decade old UI toolkit

    • Bug fixes in the framework require OS
    updates

    • Multiple tech stacks

    • Multiple sources of truth

    • Accidental complexity

    View Slide

  12. Hiccups
    • A decade old UI toolkit

    • Bug fixes in the framework require OS
    updates

    • Multiple tech stacks

    • Multiple sources of truth

    • Accidental complexity

    View Slide

  13. Hiccups
    • A decade old UI toolkit

    • Bug fixes in the framework require OS
    updates

    • Multiple tech stacks

    • Multiple sources of truth

    • Accidental complexity

    View Slide

  14. Hiccups
    • A decade old UI toolkit

    • Bug fixes in the framework require OS
    updates

    • Multiple tech stacks

    • Multiple sources of truth

    • Accidental complexity

    View Slide

  15. Hiccups
    • A decade old UI toolkit

    • Bug fixes in the framework require OS
    updates

    • Multiple tech stacks

    • Multiple sources of truth

    • Accidental complexity

    View Slide

  16. View Slide

  17. Data Binding

    View Slide

  18. View Slide

  19. View Slide

  20. View Slide

  21. Hiccups
    • A decade old UI toolkit

    • Bug fixes in the framework require OS
    updates

    • Multiple tech stacks

    • Multiple sources of truth

    • Accidental complexity

    View Slide

  22. Hiccups
    • A decade old UI toolkit

    • Bug fixes in the framework require OS
    updates

    • Multiple tech stacks

    • Multiple sources of truth

    • Accidental complexity

    View Slide

  23. View Slide

  24. Loading

    View Slide

  25. Loading Show Results

    View Slide

  26. Loading Show Results Load More

    View Slide

  27. Loading Show Results Load More

    View Slide

  28. Loading Show Results Load More

    View Slide

  29. Loading Show Results Load More

    View Slide

  30. Progress Bar?

    View Slide

  31. Loading Show Results Load More

    View Slide

  32. Loading Show Results Load More

    View Slide

  33. Loading Show Results Load More

    View Slide

  34. Loading Show Results Load More

    View Slide

  35. Loading Show Results Load More

    View Slide

  36. Loading Show Results Load More
    Loading Show Results Load More

    View Slide

  37. Loading Show Results Load More
    Loading Show Results Load More

    View Slide

  38. Loading Show Results Load More
    Loading Show Results Load More

    View Slide

  39. Loading Show Results Load More
    Loading Show Results Load More

    View Slide

  40. Loading Show Results Load More
    Loading Show Results Load More

    View Slide

  41. Hiccups
    • A decade old UI toolkit

    • Bug fixes in the framework require OS
    updates

    • Multiple tech stacks

    • Multiple sources of truth

    • Accidental complexity

    View Slide

  42. Hiccups
    • A decade old UI toolkit

    • Bug fixes in the framework require OS
    updates

    • Multiple tech stacks

    • Multiple sources of truth

    • Accidental complexity

    View Slide

  43. Jetpack Compose

    View Slide

  44. • Component-based UI framework

    • Declarative

    • Favours composition over inheritance

    • Unbundled UI Toolkit

    • Kotlin only

    • “Pre-Alpha” ⛔

    View Slide

  45. • Component-based UI framework

    • Declarative

    • Favours composition over inheritance

    • Unbundled UI Toolkit

    • Kotlin only

    • “Pre-Alpha” ⛔

    View Slide

  46. • Component-based UI framework

    • Declarative

    • Favours composition over inheritance

    • Unbundled UI Toolkit

    • Kotlin only

    • “Pre-Alpha” ⛔

    View Slide

  47. • Component-based UI framework

    • Declarative

    • Favours composition over inheritance

    • Unbundled UI Toolkit

    • Kotlin only

    • “Pre-Alpha” ⛔

    View Slide

  48. • Component-based UI framework

    • Declarative

    • Favours composition over inheritance

    • Unbundled UI Toolkit

    • Kotlin only

    • “Pre-Alpha” ⛔

    View Slide

  49. • Component-based UI framework

    • Declarative

    • Favours composition over inheritance

    • Unbundled UI Toolkit

    • Kotlin only

    • “Pre-Alpha” ⚠

    View Slide

  50. • Component-based UI framework

    • Declarative

    • Favours composition over inheritance

    • Unbundled UI Toolkit

    • Kotlin only

    • “Pre-Alpha” ⚠

    View Slide

  51. Components
    1. Compose Runtime

    2. Compose Compiler Plugin

    3. Compose UI

    View Slide

  52. Components
    1. Compose Runtime

    2. Compose Compiler Plugin

    3. Compose UI

    View Slide

  53. Components
    1. Compose Runtime

    2. Compose Compiler Plugin

    3. Compose UI

    View Slide

  54. Components
    1. Compose Runtime

    2. Compose Compiler Plugin

    3. Compose UI

    View Slide

  55. Components
    1. Compose Runtime

    2. Compose Compiler Plugin

    3. Compose UI

    View Slide

  56. Mental Model

    View Slide

  57. ui = f(s)

    View Slide

  58. ui = f(s)

    View Slide

  59. ui = f(s)

    View Slide

  60. ui = f(s)

    View Slide

  61. ui = f(s)

    View Slide

  62. No more references to
    views

    View Slide

  63. @Composable
    fun Text(
    text: String,
    style: TextStyle? = null,
    textAlign: TextAlign = DefaultTextAlign,
    // More parameters…
    ) {
    // More code…
    }
    Composable functions

    View Slide

  64. @Composable
    fun Text(
    text: String,
    style: TextStyle? = null,
    textAlign: TextAlign = DefaultTextAlign,
    // More parameters…
    ) {
    // More code…
    }
    Composable functions

    View Slide

  65. @Composable
    fun Text(
    text: String,
    style: TextStyle? = null,
    textAlign: TextAlign = DefaultTextAlign,
    // More parameters…
    ) {
    // More code…
    }
    Composable functions

    View Slide

  66. @Composable
    fun Text(
    text: String,
    style: TextStyle? = null,
    textAlign: TextAlign = DefaultTextAlign,
    // More parameters…
    ) {
    // More code…
    }
    Composable functions

    View Slide

  67. A Composed Example
    (pun intended)

    View Slide


  68. View Slide

  69. View Slide

  70. View Slide

  71. View Slide

  72. View Slide

  73. ui = f(s)

    View Slide

  74. ui = f(s)

    View Slide

  75. @Model
    data class Fruit(
    val name: String,
    val price: Double = 2.0,
    var quantity: Int = 0
    ) {
    val totalPrice: Double
    get() = quantity * price
    }

    View Slide

  76. @Model
    data class Fruit(
    val name: String,
    val price: Double = 2.0,
    var quantity: Int = 0
    ) {
    val totalPrice: Double
    get() = quantity * price
    }

    View Slide

  77. @Model
    data class Fruit(
    val name: String,
    val price: Double = 2.0,
    var quantity: Int = 0
    ) {
    val totalPrice: Double
    get() = quantity * price
    }

    View Slide

  78. @Model
    data class Fruit(
    val name: String,
    val price: Double = 2.0,
    var quantity: Int = 0
    ) {
    val totalPrice: Double
    get() = quantity * price
    }

    View Slide

  79. @Model
    data class Fruit(
    val name: String,
    val price: Double = 2.0,
    var quantity: Int = 0
    ) {
    val totalPrice: Double
    get() = quantity * price
    }

    View Slide

  80. @Model
    data class Fruit(
    val name: String,
    val price: Double = 2.0,
    var quantity: Int = 0
    ) {
    val totalPrice: Double
    get() = quantity * price
    }

    View Slide

  81. @Model
    data class Fruit(
    val name: String,
    val price: Double = 2.0,
    var quantity: Int = 0
    ) {
    val totalPrice: Double
    get() = quantity * price
    }

    View Slide

  82. @Model
    data class Fruit(
    val name: String,
    val price: Double = 2.0,
    var quantity: Int = 0
    ) {
    val totalPrice: Double
    get() = quantity * price
    }

    View Slide

  83. @Model
    data class Fruit(
    val name: String,
    val price: Double = 2.0,
    var quantity: Int = 0
    ) {
    val totalPrice: Double
    get() = quantity * price
    }

    View Slide

  84. @Model
    data class Fruit(
    val name: String,
    val price: Double = 2.0,
    var quantity: Int = 0
    ) {
    val totalPrice: Double
    get() = quantity * price
    }

    View Slide

  85. @Model
    data class Fruit(
    val name: String,
    val price: Double = 2.0,
    var quantity: Int = 0
    ) {
    val totalPrice: Double
    get() = quantity * price
    }

    View Slide

  86. @Model
    data class Fruit(
    val name: String,
    val price: Double = 2.0,
    var quantity: Int = 0
    ) {
    val totalPrice: Double
    get() = quantity * price
    }

    View Slide

  87. ui = f(s)

    View Slide

  88. ui = f(s) ✅

    View Slide

  89. ui = f(s)

    View Slide

  90. ui = f(s)

    View Slide

  91. View Slide

  92. Stepper

    View Slide

  93. @Composable
    fun Stepper(count: Int, onCountChange: (Int) P> Unit) {
    val onRemoveOne = { onCountChange(count - 1) }
    val onAddOne = { onCountChange(count + 1) }
    Row {
    Button(text = "-", onClick = if (count > 0) onRemoveOne else null)
    WidthSpacer(8.dp)
    Container(width = 24.dp) {
    Text(text = "$count", style = +themeTextStyle { body1 })
    }
    WidthSpacer(8.dp)
    Button(text = "+", onClick = onAddOne)
    }
    }

    View Slide

  94. @Composable
    fun Stepper(count: Int, onCountChange: (Int) P> Unit) {
    val onRemoveOne = { onCountChange(count - 1) }
    val onAddOne = { onCountChange(count + 1) }
    Row {
    Button(text = "-", onClick = if (count > 0) onRemoveOne else null)
    WidthSpacer(8.dp)
    Container(width = 24.dp) {
    Text(text = "$count", style = +themeTextStyle { body1 })
    }
    WidthSpacer(8.dp)
    Button(text = "+", onClick = onAddOne)
    }
    }

    View Slide

  95. @Composable
    fun Stepper(count: Int, onCountChange: (Int) P> Unit) {
    val onRemoveOne = { onCountChange(count - 1) }
    val onAddOne = { onCountChange(count + 1) }
    Row {
    Button(text = "-", onClick = if (count > 0) onRemoveOne else null)
    WidthSpacer(8.dp)
    Container(width = 24.dp) {
    Text(text = "$count", style = +themeTextStyle { body1 })
    }
    WidthSpacer(8.dp)
    Button(text = "+", onClick = onAddOne)
    }
    }

    View Slide

  96. @Composable
    fun Stepper(count: Int, onCountChange: (Int) P> Unit) {
    val onRemoveOne = { onCountChange(count - 1) }
    val onAddOne = { onCountChange(count + 1) }
    Row {
    Button(text = "-", onClick = if (count > 0) onRemoveOne else null)
    WidthSpacer(8.dp)
    Container(width = 24.dp) {
    Text(text = "$count", style = +themeTextStyle { body1 })
    }
    WidthSpacer(8.dp)
    Button(text = "+", onClick = onAddOne)
    }
    }

    View Slide

  97. @Composable
    fun Stepper(count: Int, onCountChange: (Int) P> Unit) {
    val onRemoveOne = { onCountChange(count - 1) }
    val onAddOne = { onCountChange(count + 1) }
    Row {
    Button(text = "-", onClick = if (count > 0) onRemoveOne else null)
    WidthSpacer(8.dp)
    Container(width = 24.dp) {
    Text(text = "$count", style = +themeTextStyle { body1 })
    }
    WidthSpacer(8.dp)
    Button(text = "+", onClick = onAddOne)
    }
    }

    View Slide

  98. @Composable
    fun Stepper(count: Int, onCountChange: (Int) P> Unit) {
    val onRemoveOne = { onCountChange(count - 1) }
    val onAddOne = { onCountChange(count + 1) }
    Row {
    Button(text = "-", onClick = if (count > 0) onRemoveOne else null)
    WidthSpacer(8.dp)
    Container(width = 24.dp) {
    Text(text = "$count", style = +themeTextStyle { body1 })
    }
    WidthSpacer(8.dp)
    Button(text = "+", onClick = onAddOne)
    }
    }

    View Slide

  99. @Composable
    fun Stepper(count: Int, onCountChange: (Int) P> Unit) {
    val onRemoveOne = { onCountChange(count - 1) }
    val onAddOne = { onCountChange(count + 1) }
    Row {
    Button(text = "-", onClick = if (count > 0) onRemoveOne else null)
    WidthSpacer(8.dp)
    Container(width = 24.dp) {
    Text(text = "$count", style = +themeTextStyle { body1 })
    }
    WidthSpacer(8.dp)
    Button(text = "+", onClick = onAddOne)
    }
    }

    View Slide

  100. @Composable
    fun Stepper(count: Int, onCountChange: (Int) P> Unit) {
    val onRemoveOne = { onCountChange(count - 1) }
    val onAddOne = { onCountChange(count + 1) }
    Row {
    Button(text = "-", onClick = if (count > 0) onRemoveOne else null)
    WidthSpacer(8.dp)
    Container(width = 24.dp) {
    Text(text = "$count", style = +themeTextStyle { body1 })
    }
    WidthSpacer(8.dp)
    Button(text = "+", onClick = onAddOne)
    }
    }

    View Slide

  101. @Composable
    fun Stepper(count: Int, onCountChange: (Int) P> Unit) {
    val onRemoveOne = { onCountChange(count - 1) }
    val onAddOne = { onCountChange(count + 1) }
    Row {
    Button(text = "-", onClick = if (count > 0) onRemoveOne else null)
    WidthSpacer(8.dp)
    Container(width = 24.dp) {
    Text(text = "$count", style = +themeTextStyle { body1 })
    }
    WidthSpacer(8.dp)
    Button(text = "+", onClick = onAddOne)
    }
    }

    View Slide

  102. @Composable
    fun Stepper(count: Int, onCountChange: (Int) P> Unit) {
    val onRemoveOne = { onCountChange(count - 1) }
    val onAddOne = { onCountChange(count + 1) }
    Row {
    Button(text = "-", onClick = if (count > 0) onRemoveOne else null)
    WidthSpacer(8.dp)
    Container(width = 24.dp) {
    Text(text = "$count", style = +themeTextStyle { body1 })
    }
    WidthSpacer(8.dp)
    Button(text = "+", onClick = onAddOne)
    }
    }

    View Slide

  103. @Composable
    fun Stepper(count: Int, onCountChange: (Int) P> Unit) {
    val onRemoveOne = { onCountChange(count - 1) }
    val onAddOne = { onCountChange(count + 1) }
    Row {
    Button(text = "-", onClick = if (count > 0) onRemoveOne else null)
    WidthSpacer(8.dp)
    Container(width = 24.dp) {
    Text(text = "$count", style = +themeTextStyle { body1 })
    }
    WidthSpacer(8.dp)
    Button(text = "+", onClick = onAddOne)
    }
    }

    View Slide

  104. @Composable
    fun Stepper(count: Int, onCountChange: (Int) P> Unit) {
    val onRemoveOne = { onCountChange(count - 1) }
    val onAddOne = { onCountChange(count + 1) }
    Row {
    Button(text = "-", onClick = if (count > 0) onRemoveOne else null)
    WidthSpacer(8.dp)
    Container(width = 24.dp) {
    Text(text = "$count", style = +themeTextStyle { body1 })
    }
    WidthSpacer(8.dp)
    Button(text = "+", onClick = onAddOne)
    }
    }

    View Slide

  105. @Composable
    fun Stepper(count: Int, onCountChange: (Int) P> Unit) {
    val onRemoveOne = { onCountChange(count - 1) }
    val onAddOne = { onCountChange(count + 1) }
    Row {
    Button(text = "-", onClick = if (count > 0) onRemoveOne else null)
    WidthSpacer(8.dp)
    Container(width = 24.dp) {
    Text(text = "$count", style = +themeTextStyle { body1 })
    }
    WidthSpacer(8.dp)
    Button(text = "+", onClick = onAddOne)
    }
    }

    View Slide

  106. @Composable
    fun Stepper(count: Int, onCountChange: (Int) P> Unit) {
    val onRemoveOne = { onCountChange(count - 1) }
    val onAddOne = { onCountChange(count + 1) }
    Row {
    Button(text = "-", onClick = if (count > 0) onRemoveOne else null)
    WidthSpacer(8.dp)
    Container(width = 24.dp) {
    Text(text = "$count", style = +themeTextStyle { body1 })
    }
    WidthSpacer(8.dp)
    Button(text = "+", onClick = onAddOne)
    }
    }

    View Slide

  107. @Composable
    fun Stepper(count: Int, onCountChange: (Int) P> Unit) {
    val onRemoveOne = { onCountChange(count - 1) }
    val onAddOne = { onCountChange(count + 1) }
    Row {
    Button(text = "-", onClick = if (count > 0) onRemoveOne else null)
    WidthSpacer(8.dp)
    Container(width = 24.dp) {
    Text(text = "$count", style = +themeTextStyle { body1 })
    }
    WidthSpacer(8.dp)
    Button(text = "+", onClick = onAddOne)
    }
    }

    View Slide

  108. @Composable
    fun Stepper(count: Int, onCountChange: (Int) P> Unit) {
    val onRemoveOne = { onCountChange(count - 1) }
    val onAddOne = { onCountChange(count + 1) }
    Row {
    Button(text = "-", onClick = if (count > 0) onRemoveOne else null)
    WidthSpacer(8.dp)
    Container(width = 24.dp) {
    Text(text = "$count", style = +themeTextStyle { body1 })
    }
    WidthSpacer(8.dp)
    Button(text = "+", onClick = onAddOne)
    }
    }

    View Slide

  109. @Composable
    fun Stepper(count: Int, onCountChange: (Int) P> Unit) {
    val onRemoveOne = { onCountChange(count - 1) }
    val onAddOne = { onCountChange(count + 1) }
    Row {
    Button(text = "-", onClick = if (count > 0) onRemoveOne else null)
    WidthSpacer(8.dp)
    Container(width = 24.dp) {
    Text(text = "$count", style = +themeTextStyle { body1 })
    }
    WidthSpacer(8.dp)
    Button(text = "+", onClick = onAddOne)
    }
    }

    View Slide

  110. @Composable
    fun Stepper(count: Int, onCountChange: (Int) P> Unit) {
    val onRemoveOne = { onCountChange(count - 1) }
    val onAddOne = { onCountChange(count + 1) }
    Row {
    Button(text = "-", onClick = if (count > 0) onRemoveOne else null)
    WidthSpacer(8.dp)
    Container(width = 24.dp) {
    Text(text = "$count", style = +themeTextStyle { body1 })
    }
    WidthSpacer(8.dp)
    Button(text = "+", onClick = onAddOne)
    }
    }

    View Slide

  111. @Composable
    fun Stepper(count: Int, onCountChange: (Int) P> Unit) {
    val onRemoveOne = { onCountChange(count - 1) }
    val onAddOne = { onCountChange(count + 1) }
    Row {
    Button(text = "-", onClick = if (count > 0) onRemoveOne else null)
    WidthSpacer(8.dp)
    Container(width = 24.dp) {
    Text(text = "$count", style = +themeTextStyle { body1 })
    }
    WidthSpacer(8.dp)
    Button(text = "+", onClick = onAddOne)
    }
    }

    View Slide

  112. @Composable
    fun Stepper(count: Int, onCountChange: (Int) P> Unit) {
    val onRemoveOne = { onCountChange(count - 1) }
    val onAddOne = { onCountChange(count + 1) }
    Row {
    Button(text = "-", onClick = if (count > 0) onRemoveOne else null)
    WidthSpacer(8.dp)
    Container(width = 24.dp) {
    Text(text = "$count", style = +themeTextStyle { body1 })
    }
    WidthSpacer(8.dp)
    Button(text = "+", onClick = onAddOne)
    }
    }

    View Slide

  113. @Composable
    fun Stepper(count: Int, onCountChange: (Int) P> Unit) {
    val onRemoveOne = { onCountChange(count - 1) }
    val onAddOne = { onCountChange(count + 1) }
    Row {
    Button(text = "-", onClick = if (count > 0) onRemoveOne else null)
    WidthSpacer(8.dp)
    Container(width = 24.dp) {
    Text(text = "$count", style = +themeTextStyle { body1 })
    }
    WidthSpacer(8.dp)
    Button(text = "+", onClick = onAddOne)
    }
    }

    View Slide

  114. @Composable
    fun Stepper(count: Int, onCountChange: (Int) P> Unit) {
    val onRemoveOne = { onCountChange(count - 1) }
    val onAddOne = { onCountChange(count + 1) }
    Row {
    Button(text = "-", onClick = if (count > 0) onRemoveOne else null)
    WidthSpacer(8.dp)
    Container(width = 24.dp) {
    Text(text = "$count", style = +themeTextStyle { body1 })
    }
    WidthSpacer(8.dp)
    Button(text = "+", onClick = onAddOne)
    }
    }

    View Slide

  115. @Composable
    fun Stepper(count: Int, onCountChange: (Int) P> Unit) {
    val onRemoveOne = { onCountChange(count - 1) }
    val onAddOne = { onCountChange(count + 1) }
    Row {
    Button(text = "-", onClick = if (count > 0) onRemoveOne else null)
    WidthSpacer(8.dp)
    Container(width = 24.dp) {
    Text(text = "$count", style = +themeTextStyle { body1 })
    }
    WidthSpacer(8.dp)
    Button(text = "+", onClick = onAddOne)
    }
    }

    View Slide

  116. @Composable
    fun Stepper(count: Int, onCountChange: (Int) P> Unit) {
    val onRemoveOne = { onCountChange(count - 1) }
    val onAddOne = { onCountChange(count + 1) }
    Row {
    Button(text = "-", onClick = if (count > 0) onRemoveOne else null)
    WidthSpacer(8.dp)
    Container(width = 24.dp) {
    Text(text = "$count", style = +themeTextStyle { body1 })
    }
    WidthSpacer(8.dp)
    Button(text = "+", onClick = onAddOne)
    }
    }

    View Slide

  117. @Composable
    fun Stepper(count: Int, onCountChange: (Int) P> Unit) {
    val onRemoveOne = { onCountChange(count - 1) }
    val onAddOne = { onCountChange(count + 1) }
    Row {
    Button(text = "-", onClick = if (count > 0) onRemoveOne else null)
    WidthSpacer(8.dp)
    Container(width = 24.dp) {
    Text(text = "$count", style = +themeTextStyle { body1 })
    }
    WidthSpacer(8.dp)
    Button(text = "+", onClick = onAddOne)
    }
    }

    View Slide

  118. @Composable
    fun Stepper(count: Int, onCountChange: (Int) P> Unit) {
    val onRemoveOne = { onCountChange(count - 1) }
    val onAddOne = { onCountChange(count + 1) }
    Row {
    Button(text = "-", onClick = if (count > 0) onRemoveOne else null)
    WidthSpacer(8.dp)
    Container(width = 24.dp) {
    Text(text = "$count", style = +themeTextStyle { body1 })
    }
    WidthSpacer(8.dp)
    Button(text = "+", onClick = onAddOne)
    }
    }

    View Slide

  119. @Composable
    fun Stepper(count: Int, onCountChange: (Int) P> Unit) {
    val onRemoveOne = { onCountChange(count - 1) }
    val onAddOne = { onCountChange(count + 1) }
    Row {
    Button(text = "-", onClick = if (count > 0) onRemoveOne else null)
    WidthSpacer(8.dp)
    Container(width = 24.dp) {
    Text(text = "$count", style = +themeTextStyle { body1 })
    }
    WidthSpacer(8.dp)
    Button(text = "+", onClick = onAddOne)
    }
    }

    View Slide

  120. Stepper

    View Slide

  121. Stepper ✅

    View Slide

  122. View Slide

  123. Fruit Row

    View Slide

  124. @Composable
    fun FruitRow(fruit: Fruit) {
    Padding(padding = EdgeInsets(left = 16.dp, right = 16.dp)) {
    FlexRow {
    expanded(1.0f) {
    Text(text = "${fruit.name} ($${fruit.totalPrice})", style = +themeTextStyle { h6 })
    }
    inflexible {
    Stepper(fruit.quantity, onCountChange = { fruit.quantity = it })
    }
    }
    }
    }

    View Slide

  125. @Composable
    fun FruitRow(fruit: Fruit) {
    Padding(padding = EdgeInsets(left = 16.dp, right = 16.dp)) {
    FlexRow {
    expanded(1.0f) {
    Text(text = "${fruit.name} ($${fruit.totalPrice})", style = +themeTextStyle { h6 })
    }
    inflexible {
    Stepper(fruit.quantity, onCountChange = { fruit.quantity = it })
    }
    }
    }
    }

    View Slide

  126. @Composable
    fun FruitRow(fruit: Fruit) {
    Padding(padding = EdgeInsets(left = 16.dp, right = 16.dp)) {
    FlexRow {
    expanded(1.0f) {
    Text(text = "${fruit.name} ($${fruit.totalPrice})", style = +themeTextStyle { h6 })
    }
    inflexible {
    Stepper(fruit.quantity, onCountChange = { fruit.quantity = it })
    }
    }
    }
    }

    View Slide

  127. @Composable
    fun FruitRow(fruit: Fruit) {
    Padding(padding = EdgeInsets(left = 16.dp, right = 16.dp)) {
    FlexRow {
    expanded(1.0f) {
    Text(text = "${fruit.name} ($${fruit.totalPrice})", style = +themeTextStyle { h6 })
    }
    inflexible {
    Stepper(fruit.quantity, onCountChange = { fruit.quantity = it })
    }
    }
    }
    }

    View Slide

  128. @Composable
    fun FruitRow(fruit: Fruit) {
    Padding(padding = EdgeInsets(left = 16.dp, right = 16.dp)) {
    FlexRow {
    expanded(1.0f) {
    Text(text = "${fruit.name} ($${fruit.totalPrice})", style = +themeTextStyle { h6 })
    }
    inflexible {
    Stepper(fruit.quantity, onCountChange = { fruit.quantity = it })
    }
    }
    }
    }

    View Slide

  129. @Composable
    fun FruitRow(fruit: Fruit) {
    Padding(padding = EdgeInsets(left = 16.dp, right = 16.dp)) {
    FlexRow {
    expanded(1.0f) {
    Text(text = "${fruit.name} ($${fruit.totalPrice})", style = +themeTextStyle { h6 })
    }
    inflexible {
    Stepper(fruit.quantity, onCountChange = { fruit.quantity = it })
    }
    }
    }
    }

    View Slide

  130. @Composable
    fun FruitRow(fruit: Fruit) {
    Padding(padding = EdgeInsets(left = 16.dp, right = 16.dp)) {
    FlexRow {
    expanded(1.0f) {
    Text(text = "${fruit.name} ($${fruit.totalPrice})", style = +themeTextStyle { h6 })
    }
    inflexible {
    Stepper(fruit.quantity, onCountChange = { fruit.quantity = it })
    }
    }
    }
    }

    View Slide

  131. @Composable
    fun FruitRow(fruit: Fruit) {
    Padding(padding = EdgeInsets(left = 16.dp, right = 16.dp)) {
    FlexRow {
    expanded(1.0f) {
    Text(text = "${fruit.name} ($${fruit.totalPrice})", style = +themeTextStyle { h6 })
    }
    inflexible {
    Stepper(fruit.quantity, onCountChange = { fruit.quantity = it })
    }
    }
    }
    }

    View Slide

  132. @Composable
    fun FruitRow(fruit: Fruit) {
    Padding(padding = EdgeInsets(left = 16.dp, right = 16.dp)) {
    FlexRow {
    expanded(1.0f) {
    Text(text = "${fruit.name} ($${fruit.totalPrice})", style = +themeTextStyle { h6 })
    }
    inflexible {
    Stepper(fruit.quantity, onCountChange = { fruit.quantity = it })
    }
    }
    }
    }

    View Slide

  133. @Composable
    fun FruitRow(fruit: Fruit) {
    Padding(padding = EdgeInsets(left = 16.dp, right = 16.dp)) {
    FlexRow {
    expanded(1.0f) {
    Text(text = "${fruit.name} ($${fruit.totalPrice})", style = +themeTextStyle { h6 })
    }
    inflexible {
    Stepper(fruit.quantity, onCountChange = { fruit.quantity = it })
    }
    }
    }
    }

    View Slide

  134. @Composable
    fun FruitRow(fruit: Fruit) {
    Padding(padding = EdgeInsets(left = 16.dp, right = 16.dp)) {
    FlexRow {
    expanded(1.0f) {
    Text(text = "${fruit.name} ($${fruit.totalPrice})", style = +themeTextStyle { h6 })
    }
    inflexible {
    Stepper(fruit.quantity, onCountChange = { fruit.quantity = it })
    }
    }
    }
    }

    View Slide

  135. @Composable
    fun FruitRow(fruit: Fruit) {
    Padding(padding = EdgeInsets(left = 16.dp, right = 16.dp)) {
    FlexRow {
    expanded(1.0f) {
    Text(text = "${fruit.name} ($${fruit.totalPrice})", style = +themeTextStyle { h6 })
    }
    inflexible {
    Stepper(fruit.quantity, onCountChange = { fruit.quantity = it })
    }
    }
    }
    }

    View Slide

  136. @Composable
    fun FruitRow(fruit: Fruit) {
    Padding(padding = EdgeInsets(left = 16.dp, right = 16.dp)) {
    FlexRow {
    expanded(1.0f) {
    Text(text = "${fruit.name} ($${fruit.totalPrice})", style = +themeTextStyle { h6 })
    }
    inflexible {
    Stepper(fruit.quantity, onCountChange = { fruit.quantity = it })
    }
    }
    }
    }

    View Slide

  137. @Composable
    fun FruitRow(fruit: Fruit) {
    Padding(padding = EdgeInsets(left = 16.dp, right = 16.dp)) {
    FlexRow {
    expanded(1.0f) {
    Text(text = "${fruit.name} ($${fruit.totalPrice})", style = +themeTextStyle { h6 })
    }
    inflexible {
    Stepper(fruit.quantity, onCountChange = { fruit.quantity = it })
    }
    }
    }
    }

    View Slide

  138. @Composable
    fun FruitRow(fruit: Fruit) {
    Padding(padding = EdgeInsets(left = 16.dp, right = 16.dp)) {
    FlexRow {
    expanded(1.0f) {
    Text(text = "${fruit.name} ($${fruit.totalPrice})", style = +themeTextStyle { h6 })
    }
    inflexible {
    Stepper(fruit.quantity, onCountChange = { fruit.quantity = it })
    }
    }
    }
    }

    View Slide

  139. @Composable
    fun FruitRow(fruit: Fruit) {
    Padding(padding = EdgeInsets(left = 16.dp, right = 16.dp)) {
    FlexRow {
    expanded(1.0f) {
    Text(text = "${fruit.name} ($${fruit.totalPrice})", style = +themeTextStyle { h6 })
    }
    inflexible {
    Stepper(fruit.quantity, onCountChange = { fruit.quantity = it })
    }
    }
    }
    }

    View Slide

  140. @Composable
    fun FruitRow(fruit: Fruit) {
    Padding(padding = EdgeInsets(left = 16.dp, right = 16.dp)) {
    FlexRow {
    expanded(1.0f) {
    Text(text = "${fruit.name} ($${fruit.totalPrice})", style = +themeTextStyle { h6 })
    }
    inflexible {
    Stepper(fruit.quantity, onCountChange = { fruit.quantity = it })
    }
    }
    }
    }

    View Slide

  141. @Composable
    fun FruitRow(fruit: Fruit) {
    Padding(padding = EdgeInsets(left = 16.dp, right = 16.dp)) {
    FlexRow {
    expanded(1.0f) {
    Text(text = "${fruit.name} ($${fruit.totalPrice})", style = +themeTextStyle { h6 })
    }
    inflexible {
    Stepper(fruit.quantity, onCountChange = { fruit.quantity = it })
    }
    }
    }
    }

    View Slide

  142. @Composable
    fun FruitRow(fruit: Fruit) {
    Padding(padding = EdgeInsets(left = 16.dp, right = 16.dp)) {
    FlexRow {
    expanded(1.0f) {
    Text(text = "${fruit.name} ($${fruit.totalPrice})", style = +themeTextStyle { h6 })
    }
    inflexible {
    Stepper(fruit.quantity, onCountChange = { fruit.quantity = it })
    }
    }
    }
    }

    View Slide

  143. Fruit Row

    View Slide

  144. Fruit Row

    View Slide

  145. State?

    View Slide

  146. FruitApp {
    val apple = +memo { Fruit("Apple", price = 2.0) }
    FruitRow(apple)
    }

    View Slide

  147. FruitApp {
    val apple = +memo { Fruit("Apple", price = 2.0) }
    FruitRow(apple)
    }

    View Slide

  148. FruitApp {
    val apple = +memo { Fruit("Apple", price = 2.0) }
    FruitRow(apple)
    }

    View Slide

  149. FruitApp {
    val apple = +memo { Fruit("Apple", price = 2.0) }
    FruitRow(apple)
    }

    View Slide

  150. FruitApp {
    val apple = +memo { Fruit("Apple", price = 2.0) }
    FruitRow(apple)
    }

    View Slide

  151. FruitApp {
    val apple = +memo { Fruit("Apple", price = 2.0) }
    FruitRow(apple)
    }

    View Slide

  152. FruitApp {
    val apple = +memo { Fruit("Apple", price = 2.0) }
    FruitRow(apple)
    }

    View Slide

  153. FruitApp {
    val apple = +memo { Fruit("Apple", price = 2.0) }
    FruitRow(apple)
    }

    View Slide

  154. Observations
    • Smallest reusable unit is a function

    • Stateless widgets (“controlled components”)

    • Single source of truth

    • Structure of the code matches the structure of the view hierarchy

    • UI was built from leaf to node

    • State stays as close to the root as possible

    • Declarative

    • Reduction in boilerplate

    View Slide

  155. Observations
    • Smallest reusable unit is a function

    • Stateless widgets (“controlled components”)

    • Single source of truth

    • Structure of the code matches the structure of the view hierarchy

    • UI was built from leaf to node

    • State stays as close to the root as possible

    • Declarative

    • Reduction in boilerplate

    View Slide

  156. Observations
    • Smallest reusable unit is a function

    • Stateless widgets (“controlled components”)

    • Single source of truth

    • Structure of the code matches the structure of the view hierarchy

    • UI was built from leaf to node

    • State stays as close to the root as possible

    • Declarative

    • Reduction in boilerplate

    View Slide

  157. Observations
    • Smallest reusable unit is a function

    • Stateless widgets (“controlled components”)

    • Single source of truth

    • Structure of the code matches the structure of the view hierarchy

    • UI was built from leaf to node

    • State stays as close to the root as possible

    • Declarative

    • Reduction in boilerplate

    View Slide

  158. Observations
    • Smallest reusable unit is a function

    • Stateless widgets (“controlled components”)

    • Single source of truth

    • Structure of the code matches the structure of the view hierarchy

    • UI was built from leaf to node

    • State stays as close to the root as possible

    • Declarative

    • Reduction in boilerplate

    View Slide

  159. Observations
    • Smallest reusable unit is a function

    • Stateless widgets (“controlled components”)

    • Single source of truth

    • Structure of the code matches the structure of the view hierarchy

    • UI was built from leaf to node

    • State stays as close to the root as possible

    • Declarative

    • Reduction in boilerplate

    View Slide

  160. Observations
    • Smallest reusable unit is a function

    • Stateless widgets (“controlled components”)

    • Single source of truth

    • Structure of the code matches the structure of the view hierarchy

    • UI was built from leaf to node

    • State stays as close to the root as possible

    • Declarative

    • Reduction in boilerplate

    View Slide

  161. Observations
    • Smallest reusable unit is a function

    • Stateless widgets (“controlled components”)

    • Single source of truth

    • Structure of the code matches the structure of the view hierarchy

    • UI was built from leaf to node

    • State stays as close to the root as possible

    • Declarative

    • Reduction in boilerplate

    View Slide

  162. Observations
    • Smallest reusable unit is a function

    • Stateless widgets (“controlled components”)

    • Single source of truth

    • Structure of the code matches the structure of the view hierarchy

    • UI was built from leaf to node

    • State stays as close to the root as possible

    • Declarative

    • Reduction in boilerplate

    View Slide

  163. Observations
    • Smallest reusable unit is a function

    • Stateless widgets (“controlled components”)

    • Single source of truth

    • Structure of the code matches the structure of the view hierarchy

    • UI was built from leaf to node

    • State stays as close to the root as possible

    • Declarative

    • Reduction in boilerplate

    View Slide

  164. View Slide

  165. Resources
    • http://intelligiblebabble.com/content-on-declarative-ui/

    • http://intelligiblebabble.com/compose-from-first-principles/

    • https://www.youtube.com/watch?v=4EFjDSijAZU

    • https://github.com/SimonSchubert/Braincup

    • https://developer.android.com/jetpack/compose

    • #compose channel at https://kotlinlang.slack.com/

    • https://speakerdeck.com/benoberkfell/prototyping-with-
    jetpack-compose?slide=67

    View Slide

  166. @ragunathjawahar
    Twitter / Medium / GitHub
    end;

    View Slide