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

Releasing faster with Kotlin multiplatform (DPE Summit 2023)

Releasing faster with Kotlin multiplatform (DPE Summit 2023)

In this talk, we discuss some new libraries and tools being used at Cash App to get faster feedback cycles for developers and release to users in hours instead of days. This is all built on Kotlin multiplatform and combines the best features of the web and native applications. It’s not a silver bullet, but an evolution of various techniques to try to solve some of mobile development’s biggest pain points.

We will cover:

* The history of the problem from the perspective of both our developers and the needs of the product.
* What is Kotlin Multiplatform and why did we choose it?
* Two open source libraries we built to make this possible: Redwood and Zipline.
* A demo of the technology in action and its real-world usage within Cash App.

Video: Coming soon.

Jake Wharton
PRO

September 21, 2023
Tweet

More Decks by Jake Wharton

Other Decks in Programming

Transcript

  1. @[email protected]
    Releasing faster with
    Kotlin multiplatform

    View Slide

  2. Previously On Cash App

    View Slide

  3. Previously On Cash App
    • Android, iOS, and web apps all developed natively

    View Slide

  4. Previously On Cash App
    • Android, iOS, and web apps all developed natively
    • Two week release trains for mobile apps

    View Slide

  5. Previously On Cash App
    • Android, iOS, and web apps all developed natively
    • Two week release trains for mobile apps
    • Rollout over one to two week period

    View Slide

  6. View Slide

  7. View Slide

  8. View Slide

  9. View Slide

  10. American Red Cross
    Payment to $redcross

    View Slide

  11. American Red Cross
    Payment to $redcross

    View Slide

  12. American Red Cross
    Payment to $redcross
    Donation

    View Slide

  13. American Red Cross
    Payment to $redcross
    Donation
    ~1 day

    View Slide

  14. American Red Cross
    Payment to $redcross
    Donation
    ~1 day
    3 - 7 days
    3 - 7 days

    View Slide

  15. View Slide

  16. View Slide

  17. View Slide

  18. View Slide

  19. View Slide

  20. View Slide

  21. View Slide

  22. View Slide

  23. View Slide

  24. View Slide

  25. View Slide

  26. Unification Goals

    View Slide

  27. Unification Goals
    • Logic to be updated outside of app store releases

    View Slide

  28. Unification Goals
    • Logic to be updated outside of app store releases
    • Render screens using existing native UI elements

    View Slide

  29. Unification Goals
    • Logic to be updated outside of app store releases
    • Render screens using existing native UI elements
    • Enable creation of new screens without prior knowledge

    View Slide

  30. Unification Goals
    • Logic to be updated outside of app store releases
    • Render screens using existing native UI elements
    • Enable creation of new screens without prior knowledge
    • Not be a regression on native screen development

    View Slide

  31. View Slide

  32. Kotlin Kotlin/Native
    Kotlin/JVM
    Kotlin/JS

    View Slide

  33. Kotlin Kotlin/Native
    Kotlin/JVM
    Kotlin/JS
    iOS
    Android
    JS VM

    View Slide

  34. Zipline

    View Slide

  35. View Slide

  36. View Slide

  37. PaymentRenderer {
    fun render(payment: Payment): String {
    }
    class PaymentRenderer
    fun render(payment: Payment): String

    }
    }

    View Slide

  38. class RealPaymentRenderer : PaymentRenderer {
    override fun render(payment: Payment): String {

    }
    }
    interface PaymentRenderer : ZiplineService {
    fun render(payment: Payment): String
    }

    View Slide

  39. class RealPaymentRenderer : PaymentRenderer {
    override fun render(payment: Payment): String {

    }
    }

    View Slide

  40. class RealPaymentRenderer : PaymentRenderer {
    override fun render(payment: Payment): String {

    }
    }
    zipline.bind(RealPaymentRenderer())

    View Slide

  41. class RealPaymentRenderer : PaymentRenderer {
    override fun render(payment: Payment): String {

    }
    }
    zipline.bind(RealPaymentRenderer())
    val renderer = zipline.take()
    println(renderer.render(Payment(…))

    View Slide

  42. class RealPaymentRenderer : PaymentRenderer {
    override fun render(payment: Payment): String {
    /* Fancy new impl */
    }
    }
    zipline.bind(RealPaymentRenderer())
    val renderer = zipline.take()
    println(renderer.render(Payment(…))

    View Slide

  43. class PaymentPresenter : Presenter {
    override fun render() {
    /* Display UI on Android + iOS somehow… */
    }
    }

    View Slide

  44. class PaymentPresenter : Presenter {
    override fun render() {
    /* Display UI on Android + iOS somehow… */
    }
    }
    ???

    View Slide

  45. Redwood

    View Slide

  46. Design System Schema
    data class Column(
    val children: () -> Unit,
    )
    data class TextInput(
    val hint: String,
    val text: String,
    val onTextChanged: (String) -> Unit,
    )

    View Slide

  47. Design System Schema
    data class Column(
    val children: () -> Unit,
    )
    data class TextInput(
    val hint: String,
    val text: String,
    val onTextChanged: (String) -> Unit,
    )

    View Slide

  48. Design System Schema
    data class Column(
    val children: () -> Unit,
    )
    data class TextInput(
    val hint: String,
    val text: String,
    val onTextChanged: (String) -> Unit,
    )

    View Slide

  49. Design System Schema
    data class Column(
    val children: () -> Unit,
    )
    data class TextInput(
    val hint: String,
    val text: String,
    val onTextChanged: (String) -> Unit,
    )

    View Slide

  50. Design System Schema
    data class Column(
    val children: () -> Unit,
    )
    data class TextInput(
    val hint: String,
    val text: String,
    val onTextChanged: (String) -> Unit,
    )

    View Slide

  51. View Slide

  52. data class Row(…)
    data class Column(…)
    data class Image(
    val url: HttpUrl,
    val size: ImageSize,
    val borderStyle: BorderStyle,
    )
    data class Text(
    val text: String,
    val font: FontFamily,
    val style: FontStyle,
    )

    View Slide

  53. data class ContactItem(
    val name: String,
    val image: HttpUrl,
    val content: () -> Unit,
    )
    data class Text(
    val text: String,
    val font: FontFamily,
    val style: FontStyle,
    )

    View Slide

  54. data class Row(…)
    data class Column(…)
    data class Image(
    val url: HttpUrl,
    val size: ImageSize,
    val borderStyle: BorderStyle,
    )
    data class Text(
    val text: String,
    val font: FontFamily,
    val style: FontStyle,
    )
    data class ContactItem(
    val name: String,
    val image: HttpUrl,
    val content: () -> Unit,
    )
    data class Text(
    val text: String,
    val font: FontFamily,
    val style: FontStyle,
    )

    View Slide

  55. Compose
    data class Column(
    val children: () -> Unit,
    )
    data class TextInput(
    val hint: String,
    val text: String,
    val onTextChanged: (String) -> Unit,
    )

    View Slide

  56. Compose
    data class Column(
    val children: () -> Unit,
    )
    data class TextInput(
    val hint: String,
    val text: String,
    val onTextChanged: (String) -> Unit,
    )
    @Composable fun Column(
    children: @Composable () -> Unit,
    ) { … }
    @Composable fun TextInput(
    hint: String,
    text: String,
    onTextChanged: (String) -> Unit,
    ) { … }

    View Slide

  57. Compose
    @Composable fun Column(
    children: @Composable () -> Unit,
    ) { … }
    @Composable fun TextInput(
    hint: String,
    text: String,
    onTextChanged: (String) -> Unit,
    ) { … }

    View Slide

  58. Compose
    Column {
    var query by remember { mutableStateOf("") }
    TextInput(
    hint = "Search",
    text = query,
    onTextChanged = { query = it },
    )
    val images = LoadImages(query)
    ScrollableColumn {
    for (image in images) {
    Image(url = image.url)
    }
    }
    }

    View Slide

  59. Widget Bindings
    data class Column(
    val children: () -> Unit,
    )
    data class TextInput(
    val hint: String,
    val text: String,
    val onTextChanged: (String) -> Unit,
    )

    View Slide

  60. Widget Bindings
    data class Column(
    val children: () -> Unit,
    )
    data class TextInput(
    val hint: String,
    val text: String,
    val onTextChanged: (String) -> Unit,
    )
    interface Column : Widget {
    val children: Widget.Children
    }
    interface TextInput : Widget {
    fun hint(hint: String)
    fun text(text: String)
    fun onTextChanged(onTextChanged: ((String) -> Unit)?)
    }

    View Slide

  61. Widget Bindings
    interface Column : Widget {
    val children: Widget.Children
    }
    interface TextInput : Widget {
    fun hint(hint: String)
    fun text(text: String)
    fun onTextChanged(onTextChanged: ((String) -> Unit)?)
    }

    View Slide

  62. Widget Bindings
    interface Widget {
    val value: T
    }
    interface Column : Widget {
    val children: Widget.Children
    }
    interface TextInput : Widget {
    fun hint(hint: String)
    fun text(text: String)
    fun onTextChanged(onTextChanged: ((String) -> Unit)?)
    }

    View Slide

  63. Widget Bindings
    interface Column : Widget {
    val children: Widget.Children
    }

    View Slide

  64. Widget Bindings
    class ViewColumn(
    override val value: LinearLayout,
    ) : Column {
    override val children = ViewGroupChildren(value)
    }
    interface Column : Widget {
    val children: Widget.Children
    }

    View Slide

  65. @Composable
    fun MessageCard(…) {
    Row(…) {
    Image(…)
    Spacer(…)
    Column {
    Text(…)
    Spacer(…)
    Surface(…) {
    Text(…)
    }
    }
    }
    }

    View Slide

  66. @Composable
    fun MessageCard(…) {
    Row(…) {
    Image(…)
    Spacer(…)
    Column {
    Text(…)
    Spacer(…)
    Surface(…) {
    Text(…)
    }
    }
    }
    }
    Root

    View Slide

  67. @Composable
    fun MessageCard(…) {
    Row(…) {
    Image(…)
    Spacer(…)
    Column {
    Text(…)
    Spacer(…)
    Surface(…) {
    Text(…)
    }
    }
    }
    }
    Root
    Row

    View Slide

  68. @Composable
    fun MessageCard(…) {
    Row(…) {
    Image(…)
    Spacer(…)
    Column {
    Text(…)
    Spacer(…)
    Surface(…) {
    Text(…)
    }
    }
    }
    }
    Root
    Row

    View Slide

  69. @Composable
    fun MessageCard(…) {
    Row(…) {
    Image(…)
    Spacer(…)
    Column {
    Text(…)
    Spacer(…)
    Surface(…) {
    Text(…)
    }
    }
    }
    }
    Root
    Row
    Image

    View Slide

  70. @Composable
    fun MessageCard(…) {
    Row(…) {
    Image(…)
    Spacer(…)
    Column {
    Text(…)
    Spacer(…)
    Surface(…) {
    Text(…)
    }
    }
    }
    }
    Root
    Row
    Spacer
    Image

    View Slide

  71. Spacer
    Image Column
    @Composable
    fun MessageCard(…) {
    Row(…) {
    Image(…)
    Spacer(…)
    Column {
    Text(…)
    Spacer(…)
    Surface(…) {
    Text(…)
    }
    }
    }
    }
    Root
    Row

    View Slide

  72. Spacer
    Image Column
    @Composable
    fun MessageCard(…) {
    Row(…) {
    Image(…)
    Spacer(…)
    Column {
    Text(…)
    Spacer(…)
    Surface(…) {
    Text(…)
    }
    }
    }
    }
    Root
    Row

    View Slide

  73. Spacer
    Image Column
    @Composable
    fun MessageCard(…) {
    Row(…) {
    Image(…)
    Spacer(…)
    Column {
    Text(…)
    Spacer(…)
    Surface(…) {
    Text(…)
    }
    }
    }
    }
    Root
    Row
    Text

    View Slide

  74. Spacer
    Image Column
    @Composable
    fun MessageCard(…) {
    Row(…) {
    Image(…)
    Spacer(…)
    Column {
    Text(…)
    Spacer(…)
    Surface(…) {
    Text(…)
    }
    }
    }
    }
    Root
    Row
    Text Spacer

    View Slide

  75. Text Spacer Surface
    Spacer
    Image Column
    @Composable
    fun MessageCard(…) {
    Row(…) {
    Image(…)
    Spacer(…)
    Column {
    Text(…)
    Spacer(…)
    Surface(…) {
    Text(…)
    }
    }
    }
    }
    Root
    Row

    View Slide

  76. Text Spacer Surface
    Spacer
    Image Column
    @Composable
    fun MessageCard(…) {
    Row(…) {
    Image(…)
    Spacer(…)
    Column {
    Text(…)
    Spacer(…)
    Surface(…) {
    Text(…)
    }
    }
    }
    }
    Root
    Row

    View Slide

  77. Text Spacer Surface
    Spacer
    Image Column
    @Composable
    fun MessageCard(…) {
    Row(…) {
    Image(…)
    Spacer(…)
    Column {
    Text(…)
    Spacer(…)
    Surface(…) {
    Text(…)
    }
    }
    }
    }
    Root
    Row
    Text

    View Slide

  78. @Composable
    fun MessageCard(…) {
    Row(…) {
    Image(…)
    Spacer(…)
    Column {
    Text(…)
    Spacer(…)
    Surface(…) {
    Text(…)
    }
    }
    }
    }
    Root
    Row
    Spacer
    Image Column
    Text Spacer Surface
    Text

    View Slide

  79. Root FrameLayout

    View Slide

  80. Root
    Row
    Spacer
    Image Column
    Text Spacer Surface
    Text
    FrameLayout
    LinearLayout
    View
    ImageView LinearLayout
    TextView View FrameLayout
    TextView

    View Slide

  81. Root
    Row
    Spacer
    Image Column
    Text Spacer Surface
    Text
    UIStackView
    UIStackView
    UIView
    UIImageView UIStackView
    UITextView UIView UIView
    UITextView

    View Slide

  82. Root
    Row
    Spacer
    Image Column
    Text Spacer Surface
    Text






    View Slide

  83. Redwood Counter Sample
    data class Text(
    val text: String?,
    )
    data class Button(
    val text: String?,
    val enabled: Boolean = true,
    val onClick: (() -> Unit)? = null,
    )

    View Slide

  84. Redwood Counter Sample
    @Composable
    fun Counter(value: Int = 0) {
    var count by remember { mutableStateOf(value) }
    Column {
    Button("-1", onClick = { count-- })
    Text("Count: $count")
    Button("+1", onClick = { count++ })
    }
    }

    View Slide

  85. Redwood Counter Sample
    @Composable
    fun Counter(value: Int = 0) {
    var count by remember { mutableStateOf(value) }
    Column {
    Button("-1", onClick = { count-- })
    Text("Count: $count")
    Button("+1", onClick = { count++ })
    }
    }

    View Slide

  86. Redwood Counter Sample
    @Composable
    fun Counter(value: Int = 0) {
    var count by remember { mutableStateOf(value) }
    Column {
    Button("-1", onClick = { count-- })
    Text("Count: $count")
    Button("+1", onClick = { count++ })
    }
    }

    View Slide

  87. Redwood Counter Sample
    @Composable
    fun Counter(value: Int = 0) {
    var count by remember { mutableStateOf(value) }
    Column {
    Button("-1", onClick = { count-- })
    Text("Count: $count")
    Button("+1", onClick = { count++ })
    }
    }

    View Slide

  88. Redwood Counter Sample
    @Composable
    fun Counter(value: Int = 0) {
    var count by remember { mutableStateOf(value) }
    Column {
    Button("-1", onClick = { count-- })
    Text("Count: $count")
    Button("+1", onClick = { count++ })
    }
    }

    View Slide

  89. Redwood Counter Sample
    class AndroidText(
    override val value: TextView,
    ) : Text {
    override fun text(text: String?) {
    value.text = text
    }
    }

    View Slide

  90. Redwood Counter Sample
    val redwoodLayout = RedwoodLayout(this)
    val composition = RedwoodComposition(
    scope = mainScope,
    view = redwoodLayout,
    provider = SchemaWidgetFactories(
    Counter = AndroidCounterWidgetFactory(this),
    RedwoodLayout = ViewRedwoodLayoutWidgetFactory(this),
    ),
    )

    View Slide

  91. Redwood Counter Sample
    val redwoodLayout = RedwoodLayout(this)
    val composition = RedwoodComposition(
    scope = mainScope,
    view = redwoodLayout,
    provider = SchemaWidgetFactories(
    Counter = AndroidCounterWidgetFactory(this),
    RedwoodLayout = ViewRedwoodLayoutWidgetFactory(this),
    ),
    )
    composition.setContent {
    Counter()
    }

    View Slide

  92. Redwood Counter Sample

    View Slide

  93. Redwood Counter Sample
    class ComposeUiText : Text<@Composable () -> Unit> {
    private var text by mutableStateOf("")
    override val value = @Composable {
    Text(text = text)
    }
    override fun text(text: String?) {
    this.text = text ?: ""
    }
    }

    View Slide

  94. Redwood Counter Sample
    val factories = SchemaWidgetFactories(
    Counter = ComposeUiCounterWidgetFactory,
    RedwoodLayout = ComposeUiRedwoodLayoutWidgetFactory(),
    )
    setContent {
    CounterTheme {
    RedwoodContent(factories) {
    Counter()
    }
    }
    }

    View Slide

  95. Redwood Counter Sample

    View Slide

  96. Redwood Counter Sample
    class HtmlText(
    override val value: HTMLSpanElement,
    ) : Text {
    override fun text(text: String?) {
    value.textContent = text
    }
    }

    View Slide

  97. Redwood Counter Sample

    View Slide

  98. Redwood Counter Sample
    class IosText : Text {
    override val value = UILabel().apply {
    textAlignment = NSTextAlignmentCenter
    }
    override fun text(text: String?) {
    value.text = text
    }
    }

    View Slide

  99. Redwood Counter Sample

    View Slide

  100. Zipline Redwood

    View Slide

  101. Zipline Redwood
    Treehouse

    View Slide

  102. composition.setContent {
    Counter()
    }
    val redwoodLayout = RedwoodLayout(this)
    val composition = RedwoodComposition(
    scope = mainScope,
    view = redwoodLayout,
    provider = SchemaWidgetFactories(
    Counter = AndroidCounterWidgetFactory(this),
    RedwoodLayout = ViewRedwoodLayoutWidgetFactory(this),
    ),
    )
    Treehouse Counter Sample

    View Slide

  103. Treehouse Counter Sample
    val redwoodLayout = RedwoodLayout(this)
    val composition = RedwoodComposition(
    scope = mainScope,
    view = redwoodLayout,
    provider = SchemaWidgetFactories(
    Counter = AndroidCounterWidgetFactory(this),
    RedwoodLayout = ViewRedwoodLayoutWidgetFactory(this),
    ),
    )
    composition.setContent {
    Counter()
    }

    View Slide

  104. Treehouse Counter Sample
    val redwoodLayout = RedwoodLayout(this)
    val composition = RedwoodComposition(
    scope = mainScope,
    view = redwoodLayout,
    provider = SchemaWidgetFactories(
    Counter = AndroidCounterWidgetFactory(this),
    RedwoodLayout = ViewRedwoodLayoutWidgetFactory(this),
    ),
    )
    composition.setContent {
    Counter()
    }

    View Slide

  105. Treehouse Counter Sample
    )
    val = Redwood (
    val redwoodLayout = RedwoodLayout(this)
    val composition = RedwoodComposition(
    scope = mainScope,
    view = redwoodLayout,
    provider = SchemaWidgetFactories(
    Counter = AndroidCounterWidgetFactory(this),
    RedwoodLayout = ViewRedwoodLayoutWidgetFactory(this),
    ),
    )
    composition.setContent {
    Counter()
    }

    View Slide

  106. val composition = RedwoodComposition(
    scope = mainScope,
    )
    composition.setContent {
    Counter()
    }
    val redwoodLayout = RedwoodLayout(this)
    val rendering = RedwoodRendering(
    view = redwoodLayout,
    provider = SchemaWidgetFactories(
    Counter = AndroidCounterWidgetFactory(this),
    RedwoodLayout = ViewRedwoodLayoutWidgetFactory(this),
    ),
    )
    Presenter
    Compose
    Widgets
    Platform UI
    Zipline

    View Slide

  107. Widgets
    Widget
    Protocol
    Zipline
    Presenter
    Platform UI
    Compose
    Compose
    Protocol

    View Slide

  108. Widgets
    Widget
    Protocol
    Zipline
    Platform UI

    View Slide

  109. Widgets
    Widget
    Protocol
    Zipline
    Platform UI
    Presenter Compose
    Compose
    Protocol

    View Slide

  110. Treehouse
    Widgets
    Widget
    Protocol
    Zipline
    Platform UI
    Presenter Compose
    Compose
    Protocol

    View Slide

  111. Treehouse Demo

    View Slide

  112. View Slide

  113. View Slide

  114. Treehouse Demo

    View Slide

  115. deviceframes.com

    View Slide

  116. Activity Money ···
    Android iOS
    CDN

    View Slide

  117. Activity Money ···
    Android iOS
    CDN

    View Slide

  118. Activity Money ···
    Android iOS
    CDN

    View Slide

  119. Activity Money ···
    Android iOS
    CDN
    ~8m

    View Slide

  120. .com/cashapp/zipline
    .com/cashapp/redwood

    View Slide

  121. @[email protected]
    Releasing faster with
    Kotlin multiplatform

    View Slide