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

Playing in the Treehouse with Redwood and Zipline (KotlinConf 2023)

Playing in the Treehouse with Redwood and Zipline (KotlinConf 2023)

Redwood is Cash App's multiplatform Compose library which targets the native UI toolkit of each platform while still sharing presentation logic. This means our app's existing Android views and iOS UIViews can be powered by common Compose and still offer a path to Compose UI and Swift UI.

Zipline is a multiplatform Javascript engine for Android, iOS, and the JVM which uses Kotlin interfaces for calls in and out of JS. This allows us to update the logic of our apps faster than going through the app store release process.

Treehouse is what we call the combination of Redwood and Zipline. By moving the Compose presentation logic into Zipline we can update the screens of our apps across Android, iOS, and web without waiting for the app store.

This talk will cover how Redwood and Zipline can work in isolation but how the combination as Treehouse is their most powerful form.

Video: https://jakewharton.com/playing-in-the-treehouse-with-redwood-and-zipline/

Jake Wharton
PRO

April 14, 2023
Tweet

More Decks by Jake Wharton

Other Decks in Technology

Transcript

  1. @[email protected]
    Playing in the Treehouse
    with Redwood and Zipline

    View Slide

  2. Redwood Zipline

    View Slide

  3. Redwood Zipline
    Treehouse

    View Slide

  4. Figma Android Eng
    iOS Eng
    Web Eng
    Redwood

    View Slide

  5. Figma Android Eng
    iOS Eng
    Web Eng
    View Impl
    UIKit Impl
    DOM Impl
    Redwood

    View Slide

  6. Figma Android Eng
    iOS Eng
    Web Eng
    View Impl
    UIKit Impl
    DOM Impl
    Redwood
    Redwood

    View Slide

  7. Figma Android Eng
    iOS Eng
    Web Eng
    View Impl
    UIKit Impl
    DOM Impl
    Redwood
    Redwood

    View Slide

  8. Schema Widgets
    Redwood

    View Slide

  9. Schema
    Widgets
    Compose
    Redwood

    View Slide

  10. Redwood Schema
    data class Column(
    val children: () -> Unit,
    )
    data class TextInput(
    val hint: String,
    val text: String,
    val onTextChanged: (String) -> Unit,
    )

    View Slide

  11. Redwood Schema
    data class Column(
    val children: () -> Unit,
    val birthday: LocalDate,
    )
    data class TextInput(
    val hint: String,
    val text: String,
    val onTextChanged: (String) -> Unit,
    )

    View Slide

  12. Redwood Schema
    import kotlinx.datetime.LocalDate
    data class Column(
    val children: () -> Unit,
    val birthday: LocalDate,
    )
    data class TextInput(
    val hint: String,
    val text: String,
    val onTextChanged: (String) -> Unit,
    )

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  17. Redwood Schema
    data class Column(
    val children: () -> Unit,
    )
    data class TextInput(
    val hint: String,
    val text: String,
    val onTextChanged: (String) -> Unit,
    )

    View Slide

  18. Redwood Schema

    View Slide

  19. Redwood Schema
    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

  20. Redwood Schema
    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

  21. Redwood Schema
    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

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

    View Slide

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

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

    View Slide

  25. Redwood 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

  26. Redwood Widgets
    data class Column(
    val children: () -> Unit,
    )
    data class TextInput(
    val hint: String,
    val text: String,
    val onTextChanged: (String) -> Unit,
    )

    View Slide

  27. Redwood Widgets
    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

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

  29. Redwood Widgets
    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

  30. Redwood Widgets
    interface Column : Widget {
    val children: Widget.Children
    }

    View Slide

  31. Redwood Widgets
    class ViewColumn : Column
    interface Column : Widget {
    val children: Widget.Children
    }

    View Slide

  32. Redwood Widgets
    class ViewColumn(
    override val value: LinearLayout,
    ) : Column
    interface Column : Widget {
    val children: Widget.Children
    }

    View Slide

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

    View Slide

  34. Redwood Widgets
    interface EmojiWidgetFactory : Widget.Factory {
    fun Column(): Column
    fun ScrollableColumn(): ScrollableColumn
    fun TextInput(): TextInput
    fun Image(): Image
    }

    View Slide

  35. Redwood Widgets
    interface EmojiWidgetFactory : Widget.Factory {
    fun Column(): Column

    }

    View Slide

  36. Redwood Widgets
    class EmojiViewWidgetFactory : EmojiWidgetFactory
    interface EmojiWidgetFactory : Widget.Factory {
    fun Column(): Column

    }

    View Slide

  37. Redwood Widgets
    class EmojiViewWidgetFactory(
    private val context: Context,
    ) : EmojiWidgetFactory
    interface EmojiWidgetFactory : Widget.Factory {
    fun Column(): Column

    }

    View Slide

  38. Redwood Widgets
    class EmojiViewWidgetFactory(
    private val context: Context,
    ) : EmojiWidgetFactory {
    override fun Column(): Column {
    }
    }
    interface EmojiWidgetFactory : Widget.Factory {
    fun Column(): Column

    }

    View Slide

  39. Redwood Widgets
    class EmojiViewWidgetFactory(
    private val context: Context,
    ) : EmojiWidgetFactory {
    override fun Column(): Column {
    return ViewColumn(LinearLayout(context))
    }
    }
    interface EmojiWidgetFactory : Widget.Factory {
    fun Column(): Column

    }

    View Slide

  40. Redwood Widgets
    class EmojiViewWidgetFactory(
    private val context: Context,
    ) : EmojiWidgetFactory {
    override fun Column(): Column {
    return ViewColumn(LinearLayout(context))
    }

    }
    interface EmojiWidgetFactory : Widget.Factory {
    fun Column(): Column

    }

    View Slide

  41. @Composable
    fun MessageCard(msg: Message) {
    Row(modifier = Modifier.padding(all = 8.dp)) {
    Image(
    painter = painterResource(R.drawable.profile_picture),
    contentDescription = null,
    modifier = Modifier
    .size(40.dp)
    .clip(CircleShape)
    .border(1.5.dp, MaterialTheme.colors.secondary, CircleShape),
    )
    Spacer(modifier = Modifier.width(8.dp))
    Column {
    Text(
    text = msg.author,
    color = MaterialTheme.colors.secondaryVariant,
    style = MaterialTheme.typography.subtitle2,
    )
    Spacer(modifier = Modifier.height(4.dp))
    Surface(shape = MaterialTheme.shapes.medium, elevation = 1.dp) {
    Text(
    text = msg.body,
    modifier = Modifier.padding(all = 4.dp),
    style = MaterialTheme.typography.body2,
    )
    }
    }
    }
    }

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  57. Root

    View Slide

  58. Root FrameLayout

    View Slide

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

    View Slide

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

    View Slide

  61. Root
    Row
    Spacer
    Image Column
    Text Spacer Surface
    Text






    View Slide

  62. Redwood Compose & Widgets
    @Composable fun TextInput(
    hint: String,
    text: String,
    onTextChanged: (String) -> Unit,
    ) { … }

    View Slide

  63. Redwood Compose & Widgets
    @Composable fun TextInput(
    hint: String,
    text: String,
    onTextChanged: (String) -> Unit,
    ) {

    ComposeNode<…>(
    factory = …,
    update = { … },
    )
    }

    View Slide

  64. Redwood Compose & Widgets
    @Composable fun TextInput(
    hint: String,
    text: String,
    onTextChanged: (String) -> Unit,
    ) {

    RedwoodComposeNode<…>(
    factory = …,
    update = { … },
    )
    }

    View Slide

  65. Redwood Compose & Widgets
    @Composable fun TextInput(
    hint: String,
    text: String,
    onTextChanged: (String) -> Unit,
    ) {

    RedwoodComposeNode, …>(
    factory = …,
    update = { … },
    )
    }

    View Slide

  66. Redwood Compose & Widgets
    @Composable fun TextInput(
    hint: String,
    text: String,
    onTextChanged: (String) -> Unit,
    ) {

    RedwoodComposeNode, TextInput<*>>(
    factory = …,
    update = { … },
    )
    }

    View Slide

  67. Redwood Compose & Widgets
    @Composable fun TextInput(
    hint: String,
    text: String,
    onTextChanged: (String) -> Unit,
    ) {

    RedwoodComposeNode, TextInput<*>>(
    factory = EmojiWidgetFactory<*>::TextInput,
    update = { … },
    )
    }

    View Slide

  68. Redwood Compose & Widgets
    @Composable fun TextInput(
    hint: String,
    text: String,
    onTextChanged: (String) -> Unit,
    ) {
    RedwoodComposeNode, TextInput<*>>(
    factory = EmojiWidgetFactory<*>::TextInput,
    update = {
    set(hint) { hint(hint) }
    set(text) { text(text) }
    set(onTextChanged) { onTextChanged(onTextChanged) }
    },
    )
    }

    View Slide

  69. Redwood Compose & Widgets
    @Composable fun TextInput(
    hint: String,
    text: String,
    onTextChanged: (String) -> Unit,
    ) {
    RedwoodComposeNode, TextInput<*>>(
    factory = EmojiWidgetFactory<*>::TextInput,
    update = {
    set(hint) { hint(hint) }
    set(text) { text(text) }
    set(onTextChanged) { onTextChanged(onTextChanged) }
    },
    )
    }

    View Slide

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

    View Slide

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

    View Slide

  72. Root
    Row
    Spacer
    Image Column
    Text Spacer Surface
    Text
    FrameLayout
    LinearLayout
    View
    ImageView LinearLayout
    TextView View FrameLayout
    TextView
    Text(msg.body, …)

    View Slide

  73. Root
    Row
    Spacer
    Image Column
    Text Spacer Surface
    Text
    FrameLayout
    LinearLayout
    View
    ImageView LinearLayout
    View FrameLayout
    TextView
    TextView
    Text(msg.body, …)

    View Slide

  74. Redwood Layout
    data class Row(
    val width: Constraint,
    val height: Constraint,
    val margin: Margin,
    val overflow: Overflow,
    val horizontalAlignment: MainAxisAlignment,
    val verticalAlignment: CrossAxisAlignment,
    val children: RowScope.() -> Unit,
    )
    data class Column(…)
    data class Spacer(…)

    View Slide

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

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

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

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

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

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

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

    View Slide

  82. Redwood Counter Sample
    val root = findViewById(android.R.id.content)!!
    val composition = RedwoodComposition(
    scope = mainScope,
    container = ViewGroupChildren(root),
    provider = SchemaWidgetFactories(
    Counter = AndroidCounterWidgetFactory(this),
    RedwoodLayout = ViewRedwoodLayoutWidgetFactory(this),
    ),
    )

    View Slide

  83. Redwood Counter Sample
    val root = findViewById(android.R.id.content)!!
    val composition = RedwoodComposition(
    scope = mainScope,
    container = ViewGroupChildren(root),
    provider = SchemaWidgetFactories(
    Counter = AndroidCounterWidgetFactory(this),
    RedwoodLayout = ViewRedwoodLayoutWidgetFactory(this),
    ),
    )

    View Slide

  84. Redwood Counter Sample
    val root = findViewById(android.R.id.content)!!
    val composition = RedwoodComposition(
    scope = mainScope,
    container = ViewGroupChildren(root),
    provider = SchemaWidgetFactories(
    Counter = AndroidCounterWidgetFactory(this),
    RedwoodLayout = ViewRedwoodLayoutWidgetFactory(this),
    ),
    )

    View Slide

  85. Redwood Counter Sample
    val root = findViewById(android.R.id.content)!!
    val composition = RedwoodComposition(
    scope = mainScope,
    container = ViewGroupChildren(root),
    provider = SchemaWidgetFactories(
    Counter = AndroidCounterWidgetFactory(this),
    RedwoodLayout = ViewRedwoodLayoutWidgetFactory(this),
    ),
    )

    View Slide

  86. Redwood Counter Sample
    val root = findViewById(android.R.id.content)!!
    val composition = RedwoodComposition(
    scope = mainScope,
    container = ViewGroupChildren(root),
    provider = SchemaWidgetFactories(
    Counter = AndroidCounterWidgetFactory(this),
    RedwoodLayout = ViewRedwoodLayoutWidgetFactory(this),
    ),
    )

    View Slide

  87. Redwood Counter Sample
    val root = findViewById(android.R.id.content)!!
    val composition = RedwoodComposition(
    scope = mainScope,
    container = ViewGroupChildren(root),
    provider = SchemaWidgetFactories(
    Counter = AndroidCounterWidgetFactory(this),
    RedwoodLayout = ViewRedwoodLayoutWidgetFactory(this),
    ),
    )
    composition.setContent {
    Counter()
    }

    View Slide

  88. Redwood Counter Sample

    View Slide

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

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

    View Slide

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

    View Slide

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

    View Slide

  93. Redwood Counter Sample

    View Slide

  94. Redwood Counter Sample
    application {
    Window(
    onCloseRequest = ::exitApplication,
    title = "Counter",
    state = rememberWindowState(300.dp, 300.dp),
    ) {
    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. Redwood
    Treehouse
    Zipline

    View Slide

  101. Zipline
    2016
    2019
    2022

    View Slide

  102. View Slide

  103. View Slide

  104. View Slide

  105. View Slide

  106. American Red Cross
    Payment to $redcross

    View Slide

  107. American Red Cross
    Payment to $redcross

    View Slide

  108. American Red Cross
    Payment to $redcross
    Donation

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  112. View Slide

  113. View Slide

  114. {"id":"Lgh13IUG7f6","amount":137,"currency":"USD","sender":
    {"id":"2q3u9bUG8f6","username":"jake","display_name":"Jake
    Wharton","is_non_profit":false},"recipient":
    {"id":"h98FUH87gwe","username":"redcross","display_name":"America
    n Red Cross","is_non_profit":true},"date":"2018-10-29T15:22:21"}

    View Slide

  115. {"id":"Lgh13IUG7f6","amount":137,"currency":"USD","sender":
    {"id":"2q3u9bUG8f6","username":"jake","display_name":"Jake
    Wharton","is_non_profit":false},"recipient":
    {"id":"h98FUH87gwe","username":"redcross","display_name":"America
    n Red Cross","is_non_profit":true},"date":"2018-10-29T15:22:21"}
    {"payment_line_1":"American Red Cross","payment_line_2":"Donation
    to $redcross","payment_line_3":"$1.37","payment_line_4":"Oct 29,
    2018 at 3:22PM"}

    View Slide

  116. {"id":"Lgh13IUG7f6","amount":137,"currency":"USD","sender":
    {"id":"2q3u9bUG8f6","username":"jake","display_name":"Jake
    Wharton","is_non_profit":false},"recipient":
    {"id":"h98FUH87gwe","username":"redcross","display_name":"America
    n Red Cross","is_non_profit":true},"date":"2018-10-29T15:22:21"}
    {"payment_line_1":"American Red Cross","payment_line_2":"Donation
    to $redcross","payment_line_3":"$1.37","payment_line_4":"Oct 29,
    2018 at 3:22PM"}
    if (payment.recipient.is_non_profit
    && payment.sender.is_florida_man) {
    return "Donation to %s"
    }
    return "Payment to %s

    View Slide

  117. View Slide

  118. View Slide

  119. View Slide

  120. View Slide

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

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

    View Slide

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

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

    View Slide

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

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

    View Slide

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

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

    View Slide

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

    }
    }

    View Slide

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

    }
    }
    zipline.bind(RealPaymentRenderer())

    View Slide

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

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

    View Slide

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

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

    View Slide

  130. zipline.bind(SystemLocationManager(…))
    // Load Kotlin/JS
    val renderer = zipline.take()
    println(renderer.render(Payment(…))
    class RealPaymentRenderer(
    private val location: LocationManager,
    ) : PaymentRenderer {
    override fun render(payment: Payment): String {
    /* Fancy new impl */
    }
    }
    val location = zipline.take()
    zipline.bind(RealPaymentRenderer(location))

    View Slide

  131. interface SomeService : ZiplineService {
    fun sync(in: String): String
    }

    View Slide

  132. interface SomeService : ZiplineService {
    fun sync(in: String): String
    suspend fun async(in: String): String
    }

    View Slide

  133. interface SomeService : ZiplineService {
    fun sync(in: String): String
    suspend fun async(in: String): String
    fun stream(in: String): Flow
    }

    View Slide

  134. interface SomeService : ZiplineService {
    fun sync(in: String): String
    suspend fun async(in: String): String
    fun stream(in: String): Flow
    fun stateStream(in: String): StateFlow
    }

    View Slide

  135. interface SomeService : ZiplineService {
    fun sync(in: String): String
    suspend fun async(in: String): String
    fun stream(in: Flow): String
    fun stateStream(in: StateFlow): String
    }

    View Slide

  136. interface SomeService : ZiplineService {
    fun sync(in: String): String
    suspend fun async(in: String): String
    fun stream(in: Flow): String
    fun stateStream(in: StateFlow): String
    fun userTypes(in: Payment): RenderedPayment
    }

    View Slide

  137. interface SomeService : ZiplineService {

    fun userTypes(in: Payment): RenderedPayment
    }
    f
    u
    n s
    y
    n
    c
    (
    i
    n
    : S
    t
    r
    i
    n
    g
    )
    : S
    t
    r
    i
    n
    g
    s
    u
    s
    p
    e
    n
    d f
    u
    n a
    s
    y
    n
    c
    (
    i
    n
    : S
    t
    r
    i
    n
    g
    )
    : S
    t
    r
    i
    n
    g
    f
    u
    n s
    t
    r
    e
    a
    m
    (
    i
    n
    : F
    l
    o
    w
    <
    S
    t
    r
    i
    n
    g
    >
    )
    : S
    t
    r
    i
    n
    g
    f
    u
    n s
    t
    a
    t
    e
    S
    t
    r
    e
    a
    m
    (
    i
    n
    : S
    t
    a
    t
    e
    F
    l
    o
    w
    <
    S
    t
    r
    i
    n
    g
    >
    )
    :S
    t
    r
    i
    n
    g

    View Slide

  138. interface SomeService : ZiplineService {

    fun userTypes(in: Payment): RenderedPayment
    internal object Adapter : ZiplineAdapter {
    }
    }

    View Slide

  139. interface SomeService : ZiplineService {

    fun userTypes(in: Payment): RenderedPayment
    internal object Adapter : ZiplineAdapter {
    override fun outboundService(handler: Handler)
    }
    }

    View Slide

  140. interface SomeService : ZiplineService {

    fun userTypes(in: Payment): RenderedPayment
    internal object Adapter : ZiplineAdapter {
    override fun outboundService(handler: Handler) =
    object : SomeService {
    override fun userTypes(in: Payment) =
    handler.call(this, in) as RenderedPayment
    }
    }
    }

    View Slide

  141. interface SomeService : ZiplineService {

    fun userTypes(in: Payment): RenderedPayment
    internal object Adapter : ZiplineAdapter {
    override fun outboundService(handler: Handler) =
    object : SomeService {
    override fun userTypes(in: Payment) =
    handler.call(this, in) as RenderedPayment
    }
    }
    }

    View Slide

  142. interface SomeService : ZiplineService {

    fun userTypes(in: Payment): RenderedPayment
    internal object Adapter : ZiplineAdapter {

    }
    }
    @Serializable
    class RenderedPayment(…)
    @Serializable
    class Payment(…)

    View Slide

  143. QuickJS
    function sayHello() {
    return ['Hello', 'JavaScript']
    .join(' ');
    }

    View Slide

  144. QuickJS
    function sayHello() {
    return ['Hello', 'JavaScript']
    .join(' ');
    }
    hello.js:1: function: sayHello
    mode: strict
    stack_size: 3
    opcodes:
    push_atom_value "Hello"
    push_atom_value "JavaScript"
    array_from 2
    get_field2 join
    push_atom_value " "
    tail_call_method 1

    View Slide

  145. QuickJS
    hello.js:1: function: sayHello
    mode: strict
    stack_size: 3
    opcodes:
    push_atom_value "Hello"
    push_atom_value "JavaScript"
    array_from 2
    get_field2 join
    push_atom_value " "
    tail_call_method 1
    "Hello, JavaScript"

    View Slide

  146. QuickJS
    hello.js:1: function: sayHello
    mode: strict
    stack_size: 3
    opcodes:
    push_atom_value "Hello"
    push_atom_value "JavaScript"
    array_from 2
    get_field2 join
    push_atom_value " "
    tail_call_method 1
    function sayHello() {
    return ['Hello', 'JavaScript']
    .join(' ');
    }
    "Hello, JavaScript"
    hello.js:1: function: sayHello
    mode: strict
    stack_size: 3
    opcodes:
    push_atom_value "Hello"
    push_atom_value "JavaScript"
    array_from 2
    get_field2 join
    push_atom_value " "
    tail_call_method 1

    View Slide

  147. hello.js:1: function: sayHello
    mode: strict
    stack_size: 3
    opcodes:
    push_atom_value "Hello"
    push_atom_value "JavaScript"
    array_from 2
    get_field2 join
    push_atom_value " "
    tail_call_method 1
    function sayHello() {
    return ['Hello', 'JavaScript']
    .join(' ');
    }
    "Hello, JavaScript"
    hello.js:1: function: sayHello
    mode: strict
    stack_size: 3
    opcodes:
    push_atom_value "Hello"
    push_atom_value "JavaScript"
    array_from 2
    get_field2 join
    push_atom_value " "
    tail_call_method 1
    Server:
    Client:

    View Slide

  148. hello.js:1: function: sayHello
    mode: strict
    stack_size: 3
    opcodes:
    push_atom_value "Hello"
    push_atom_value "JavaScript"
    array_from 2
    get_field2 join
    push_atom_value " "
    tail_call_method 1
    function sayHello() {
    return ['Hello', 'JavaScript']
    .join(' ');
    }
    fun sayHello() {
    return arrayOf("Hello", "JavaScript")
    .asDynamic()
    .join(" ")
    }

    View Slide

  149. hello.js:1: function: sayHello
    mode: strict
    stack_size: 3
    opcodes:
    push_atom_value "Hello"
    push_atom_value "JavaScript"
    array_from 2
    get_field2 join
    push_atom_value " "
    tail_call_method 1
    function sayHello() {
    return ['Hello', 'JavaScript']
    .join(' ');
    }
    fun sayHello() {
    return arrayOf("Hello", "JavaScript")
    .asDynamic()
    .join(" ")
    }

    View Slide

  150. hello.js:1: function: sayHello
    mode: strict
    stack_size: 3
    opcodes:
    push_atom_value "Hello"
    push_atom_value "JavaScript"
    array_from 2
    get_field2 join
    push_atom_value " "
    tail_call_method 1
    function sayHello() {
    return ['Hello', 'JavaScript']
    .join(' ');
    }
    fun sayHello() {
    return arrayOf("Hello", "JavaScript")
    .asDynamic()
    .join(" ")
    }
    {"version":3,"sources":
    ["../../src/commonMain/
    kotlin/com/example/
    Hello.kt"],"sourcesCont
    ent":[null],,"names":
    [],"mappings":";IAsBE,g
    C;IAMA,wB;IAMA,4B;"}

    View Slide

  151. hello.js:1: function: sayHello
    mode: strict
    stack_size: 3
    opcodes:
    push_atom_value "Hello"
    push_atom_value "JavaScript"
    array_from 2
    get_field2 join
    push_atom_value " "
    tail_call_method 1
    function sayHello() {
    return ['Hello', 'JavaScript']
    .join(' ');
    }
    fun sayHello() {
    return arrayOf("Hello", "JavaScript")
    .asDynamic()
    .join(" ")
    }
    {"version":3,"sources":
    ["../../src/commonMain/
    kotlin/com/example/
    Hello.kt"],"sourcesCont
    ent":[null],,"names":
    [],"mappings":";IAsBE,g
    C;IAMA,wB;IAMA,4B;"}

    View Slide

  152. Hello.kt:1: function: sayHello
    mode: strict
    stack_size: 3
    opcodes:
    push_atom_value "Hello"
    push_atom_value "JavaScript"
    array_from 2
    get_field2 join
    push_atom_value " "
    tail_call_method 1
    function sayHello() {
    return ['Hello', 'JavaScript']
    .join(' ');
    }
    fun sayHello() {
    return arrayOf("Hello", "JavaScript")
    .asDynamic()
    .join(" ")
    }
    {"version":3,"sources":
    ["../../src/commonMain/
    kotlin/com/example/
    Hello.kt"],"sourcesCont
    ent":[null],,"names":
    [],"mappings":";IAsBE,g
    C;IAMA,wB;IAMA,4B;"}

    View Slide

  153. Hello.kt:1: function: sayHello
    mode: strict
    stack_size: 3
    opcodes:
    push_atom_value "Hello"
    push_atom_value "JavaScript"
    array_from 2
    get_field2 join
    push_atom_value " "
    tail_call_method 1
    function sayHello() {
    return ['Hello', 'JavaScript']
    .join(' ');
    }
    fun sayHello() {
    return arrayOf("Hello", "JavaScript")
    .asDynamic()
    .join(" ")
    }
    {"version":3,"sources":
    ["../../src/commonMain/
    kotlin/com/example/
    Hello.kt"],"sourcesCont
    ent":[null],,"names":
    [],"mappings":";IAsBE,g
    C;IAMA,wB;IAMA,4B;"}

    View Slide

  154. java.lang.Exception: unexpected timezone: America/Gotham
    at captureStack (runtime/coreRuntime.kt:86)
    at IllegalStateException_init (kotlin-kotlin-stdlib-js-ir.js)
    at (app.cash.worldclock/TimeFormatter.kt:98)
    at (app.cash.worldclock/WorldClockJs.kt:615)
    at (InboundService.kt:837)
    at (zipline-root-zipline.js)
    at app.cash.zipline.OutboundService.runJob(platform.kt:37)
    at app.cash.zipline.EventLoop.run(CoroutineEventLoop.kt:57)
    at j.u.concurrent.Executor.runWorker(ThreadPoolExecutor.java:1167)
    at j.u.concurrent.Executor$Worker.run(ThreadPoolExecutor.java:641)
    at java.lang.Thread.run(Thread.java:920)

    View Slide

  155. java.lang.Exception: unexpected timezone: America/Gotham
    at captureStack (runtime/coreRuntime.kt:86)
    at IllegalStateException_init (kotlin-kotlin-stdlib-js-ir.js)
    at (app.cash.worldclock/TimeFormatter.kt:98)
    at (app.cash.worldclock/WorldClockJs.kt:615)
    at (InboundService.kt:837)
    at (zipline-root-zipline.js)
    at app.cash.zipline.OutboundService.runJob(platform.kt:37)
    at app.cash.zipline.EventLoop.run(CoroutineEventLoop.kt:57)
    at j.u.concurrent.Executor.runWorker(ThreadPoolExecutor.java:1167)
    at j.u.concurrent.Executor$Worker.run(ThreadPoolExecutor.java:641)
    at java.lang.Thread.run(Thread.java:920)

    View Slide

  156. Zipline
    JavaScript VM

    View Slide

  157. Zipline
    JavaScript VM
    2015 - 2023
    WASM VM
    Coming 2024

    View Slide

  158. Redwood Zipline
    Treehouse

    View Slide

  159. Treehouse
    val root = findViewById(android.R.id.content)!!
    val composition = RedwoodComposition(
    scope = mainScope,
    container = ViewGroupChildren(root),
    provider = SchemaWidgetFactories(
    Counter = AndroidCounterWidgetFactory(this),
    RedwoodLayout = ViewRedwoodLayoutWidgetFactory(this),
    ),
    )
    composition.setContent {
    Counter()
    }
    Treehouse Counter Sample

    View Slide

  160. Treehouse Counter Sample
    val root = findViewById(android.R.id.content)!!
    val composition = RedwoodComposition(
    scope = mainScope,
    container = ViewGroupChildren(root),
    provider = SchemaWidgetFactories(
    Counter = AndroidCounterWidgetFactory(this),
    RedwoodLayout = ViewRedwoodLayoutWidgetFactory(this),
    ),
    )
    composition.setContent {
    Counter()
    }

    View Slide

  161. Treehouse Counter Sample
    val root = findViewById(android.R.id.content)!!
    val composition = RedwoodComposition(
    scope = mainScope,
    container = ViewGroupChildren(root),
    provider = SchemaWidgetFactories(
    Counter = AndroidCounterWidgetFactory(this),
    RedwoodLayout = ViewRedwoodLayoutWidgetFactory(this),
    ),
    )
    composition.setContent {
    Counter()
    }

    View Slide

  162. Treehouse Counter Sample
    )
    val root = findViewById(android.R.id.content)!!
    val composition = RedwoodComposition(
    scope = mainScope,
    container = ViewGroupChildren(root),
    provider = SchemaWidgetFactories(
    Counter = AndroidCounterWidgetFactory(this),
    RedwoodLayout = ViewRedwoodLayoutWidgetFactory(this),
    ),
    )
    composition.setContent {
    Counter()
    }
    val = Redwood (

    View Slide

  163. val composition = RedwoodComposition(
    scope = mainScope,
    )
    composition.setContent {
    Counter()
    }
    val root = findViewById(android.R.id.content)!!
    val rendering = RedwoodRendering(
    container = ViewGroupChildren(root),
    provider = SchemaWidgetFactories(
    Counter = AndroidCounterWidgetFactory(this),
    RedwoodLayout = ViewRedwoodLayoutWidgetFactory(this),
    ),
    )
    Presenter
    Compose
    Widgets
    Platform UI
    Zipline

    View Slide

  164. val composition = RedwoodComposition(
    scope = mainScope,
    )
    composition.setContent {
    Counter()
    }
    Widgets
    Zipline
    Presenter
    Platform UI
    Compose
    val root = findViewById(android.R.id.content)!!
    val container = ViewGroupChildren(root)
    val provider = SchemaWidgetFactories(
    Counter = AndroidCounterWidgetFactory(this),
    RedwoodLayout = ViewRedwoodLayoutWidgetFactory(this),
    )
    val rendering = RedwoodRendering(
    container = container,
    provider = provider,
    )

    View Slide

  165. val composition = RedwoodComposition(
    scope = mainScope,
    )
    composition.setContent {
    Counter()
    }
    Widgets
    Zipline
    Presenter
    Platform UI
    Compose
    val root = findViewById(android.R.id.content)!!
    val container = ViewGroupChildren(root)
    val provider = SchemaWidgetFactories(
    Counter = AndroidCounterWidgetFactory(this),
    RedwoodLayout = ViewRedwoodLayoutWidgetFactory(this),
    )
    val rendering = RedwoodRendering(
    container = container,
    provider = provider,
    )

    View Slide

  166. val composition = RedwoodComposition(
    scope = mainScope,
    )
    composition.setContent {
    Counter()
    }
    Widgets
    Zipline
    Presenter
    Platform UI
    Compose
    val root = findViewById(android.R.id.content)!!
    val container = ViewGroupChildren(root)
    val provider = SchemaWidgetFactories(
    Counter = AndroidCounterWidgetFactory(this),
    RedwoodLayout = ViewRedwoodLayoutWidgetFactory(this),
    )
    val rendering = RedwoodRendering(
    container = container,
    provider = provider,
    )

    View Slide

  167. val composition = RedwoodComposition(
    scope = mainScope,
    )
    composition.setContent {
    Counter()
    }
    Widgets
    kotlinx
    serialization
    Zipline
    Presenter
    Platform UI
    Compose
    kotlinx
    serialization
    val root = findViewById(android.R.id.content)!!
    val container = ViewGroupChildren(root)
    val provider = SchemaWidgetFactories(
    Counter = AndroidCounterWidgetFactory(this),
    RedwoodLayout = ViewRedwoodLayoutWidgetFactory(this),
    )
    val rendering = RedwoodRendering(
    container = container,
    provider = provider,
    )

    View Slide

  168. Schema
    Widgets
    Compose

    View Slide

  169. Schema
    Widgets
    Compose
    Compose
    Protocol
    Widget
    Protocol

    View Slide

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

    View Slide

  171. Widgets
    Widget
    Protocol
    Zipline
    Platform UI

    View Slide

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

    View Slide

  173. Redwood Protocol
    data class Column(
    val children: () -> Unit,
    )
    data class TextInput(
    val hint: String,
    val text: String,
    val onTextChanged: (String) -> Unit,
    )

    View Slide

  174. Redwood Protocol
    @Widget(1)
    data class Column(
    val children: () -> Unit,
    )
    @Widget(2)
    data class TextInput(
    val hint: String,
    val text: String,
    val onTextChanged: (String) -> Unit,
    )

    View Slide

  175. Redwood Protocol
    @Widget(1)
    data class Column(
    val children: () -> Unit,
    )
    @Widget(2)
    data class TextInput(
    @Property(1) val hint: String,
    @Property(2) val text: String,
    @Property(3) val onTextChanged: (String) -> Unit,
    )

    View Slide

  176. Redwood Protocol
    @Widget(1)
    data class Column(
    @Children(1) val children: () -> Unit,
    )
    @Widget(2)
    data class TextInput(
    @Property(1) val hint: String,
    @Property(2) val text: String,
    @Property(3) val onTextChanged: (String) -> Unit,
    )

    View Slide

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

    View Slide

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

    View Slide

  179. Treehouse Demo

    View Slide

  180. View Slide

  181. Treehouse Demo

    View Slide

  182. Redwood Zipline
    Treehouse

    View Slide

  183. deviceframes.com

    View Slide

  184. .com/cashapp/zipline
    .com/cashapp/redwood

    View Slide

  185. @[email protected]
    Playing in the Treehouse
    with Redwood and Zipline

    View Slide