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

Releasing faster with Kotlin multiplatform (DPE...

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.

Jake Wharton

September 21, 2023
Tweet

Video

More Decks by Jake Wharton

Other Decks in Programming

Transcript

  1. Previously On Cash App • Android, iOS, and web apps

    all developed natively • Two week release trains for mobile apps
  2. 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
  3. Unification Goals • Logic to be updated outside of app

    store releases • Render screens using existing native UI elements
  4. 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
  5. 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
  6. class RealPaymentRenderer : PaymentRenderer { override fun render(payment: Payment): String

    { … } } interface PaymentRenderer : ZiplineService { fun render(payment: Payment): String }
  7. class RealPaymentRenderer : PaymentRenderer { override fun render(payment: Payment): String

    { … } } zipline.bind<PaymentRenderer>(RealPaymentRenderer())
  8. class RealPaymentRenderer : PaymentRenderer { override fun render(payment: Payment): String

    { … } } zipline.bind<PaymentRenderer>(RealPaymentRenderer()) val renderer = zipline.take<PaymentRenderer>() println(renderer.render(Payment(…))
  9. class RealPaymentRenderer : PaymentRenderer { override fun render(payment: Payment): String

    { /* Fancy new impl */ } } zipline.bind<PaymentRenderer>(RealPaymentRenderer()) val renderer = zipline.take<PaymentRenderer>() println(renderer.render(Payment(…))
  10. class PaymentPresenter : Presenter { override fun render() { /*

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

    Display UI on Android + iOS somehow… */ } } ???
  12. Design System Schema data class Column( val children: () ->

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

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

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

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

    Unit, ) data class TextInput( val hint: String, val text: String, val onTextChanged: (String) -> Unit, )
  17. 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, )
  18. data class ContactItem( val name: String, val image: HttpUrl, val

    content: () -> Unit, ) data class Text( val text: String, val font: FontFamily, val style: FontStyle, )
  19. 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, )
  20. Compose data class Column( val children: () -> Unit, )

    data class TextInput( val hint: String, val text: String, val onTextChanged: (String) -> Unit, )
  21. 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, ) { … }
  22. Compose @Composable fun Column( children: @Composable () -> Unit, )

    { … } @Composable fun TextInput( hint: String, text: String, onTextChanged: (String) -> Unit, ) { … }
  23. 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) } } }
  24. Widget Bindings data class Column( val children: () -> Unit,

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

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

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

    } interface Column<T : Any> : Widget<T> { val children: Widget.Children<T> } interface TextInput<T : Any> : Widget<T> { fun hint(hint: String) fun text(text: String) fun onTextChanged(onTextChanged: ((String) -> Unit)?) }
  28. Widget Bindings class ViewColumn( override val value: LinearLayout, ) :

    Column<View> { override val children = ViewGroupChildren(value) } interface Column<T : Any> : Widget<T> { val children: Widget.Children<T> }
  29. @Composable fun MessageCard(…) { Row(…) { Image(…) Spacer(…) Column {

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

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

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

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

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

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

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

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

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

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

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

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

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

    Text(…) Spacer(…) Surface(…) { Text(…) } } } } Root Row Spacer Image Column Text Spacer Surface Text
  43. Root Row Spacer Image Column Text Spacer Surface Text FrameLayout

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

    UIStackView UIView UIImageView UIStackView UITextView UIView UIView UITextView
  45. Root Row Spacer Image Column Text Spacer Surface Text <div>

    <div> <div> <img> <div> <span> <div> <div> <span>
  46. Redwood Counter Sample data class Text( val text: String?, )

    data class Button( val text: String?, val enabled: Boolean = true, val onClick: (() -> Unit)? = null, )
  47. 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++ }) } }
  48. 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++ }) } }
  49. 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++ }) } }
  50. 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++ }) } }
  51. 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++ }) } }
  52. Redwood Counter Sample class AndroidText( override val value: TextView, )

    : Text<View> { override fun text(text: String?) { value.text = text } }
  53. Redwood Counter Sample val redwoodLayout = RedwoodLayout(this) val composition =

    RedwoodComposition( scope = mainScope, view = redwoodLayout, provider = SchemaWidgetFactories( Counter = AndroidCounterWidgetFactory(this), RedwoodLayout = ViewRedwoodLayoutWidgetFactory(this), ), )
  54. 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() }
  55. 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 ?: "" } }
  56. Redwood Counter Sample val factories = SchemaWidgetFactories( Counter = ComposeUiCounterWidgetFactory,

    RedwoodLayout = ComposeUiRedwoodLayoutWidgetFactory(), ) setContent { CounterTheme { RedwoodContent(factories) { Counter() } } }
  57. Redwood Counter Sample class HtmlText( override val value: HTMLSpanElement, )

    : Text<HTMLElement> { override fun text(text: String?) { value.textContent = text } }
  58. Redwood Counter Sample class IosText : Text<UIView> { override val

    value = UILabel().apply { textAlignment = NSTextAlignmentCenter } override fun text(text: String?) { value.text = text } }
  59. 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
  60. 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() }
  61. 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() }
  62. 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() }
  63. 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