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

Native UI with Multiplatform Compose (Droidcon NYC 2022)

Jake Wharton
PRO
September 01, 2022

Native UI with Multiplatform Compose (Droidcon NYC 2022)

Compose is a delightful way to build UI in Kotlin, but Compose UI doesn't always make sense (or exist) on platforms like the web and iOS. React Native uses the native UI toolkit of each platform, but requires JavaScript and is always chasing compatibility with each platform's new features.

Redwood is a new tool from Cash App which lets you keep writing Kotlin and Compose but target the native UI toolkit of each platform.

This session will introduce Redwood by:
- Comparing and contrasting it with other cross-platform tools
- Demonstrate its differentiating features in samples and production usage
- Document the internal architecture of how it works
- Using it to update screens outside of the app store process

Video: https://jakewharton.com/native-ui-with-multiplatform-compose/

Jake Wharton
PRO

September 01, 2022
Tweet

More Decks by Jake Wharton

Other Decks in Technology

Transcript

  1. @jakewharton
    @jessewilson
    Native UI with
    Multiplatform Compose

    View Slide

  2. Demo

    View Slide

  3. Redwood
    • Kotlin multiplatform (Android, JVM, JS, iOS, MacOS)

    View Slide

  4. Redwood
    • Kotlin multiplatform (Android, JVM, JS, iOS, MacOS)
    • Powered by Compose

    View Slide

  5. Redwood
    • Kotlin multiplatform (Android, JVM, JS, iOS, MacOS)
    • Powered by Compose
    • Shared presenter logic across all platforms

    View Slide

  6. Redwood
    • Kotlin multiplatform (Android, JVM, JS, iOS, MacOS)
    • Powered by Compose
    • Shared presenter logic across all platforms
    • Uses native UI toolkit specific to each platform

    View Slide

  7. Redwood
    • Kotlin multiplatform (Android, JVM, JS, iOS, MacOS)
    • Powered by Compose
    • Shared presenter logic across all platforms
    • Uses native UI toolkit specific to each platform
    • Not an all-or-nothing framework

    View Slide

  8. 2020 2021 2022
    ay June July August September

    View Slide

  9. 2012 2013 2014
    September October November Dece
    • Native UI feels best

    View Slide

  10. 2012 2013 2014
    September October November Dece
    • Native UI feels best
    • Design system is cross platform

    View Slide

  11. 2012 2013 2014
    September October November Dece
    • Native UI feels best
    • Design system is cross platform
    • Hire the best engineers on each platform

    View Slide

  12. 2014 2015 2016 2017 2018
    ay June July August September October November Dece
    • Code sharing is awesome

    View Slide

  13. 2014 2015 2016 2017 2018
    ay June July August September October November Dece
    • Code sharing is awesome
    • Use JS engine to share logic which changes frequently

    View Slide

  14. 2014 2015 2016 2017 2018
    ay June July August September October November Dece
    • Code sharing is awesome
    • Use JS engine to share logic which changes frequently
    • Duktape-Android library from Square (Droidcon talk)

    View Slide

  15. ay June July August September October November Dece
    2016 2017 2018 2019 2020
    • Investing in Kotlin/MPP as the future

    View Slide

  16. ay June July August September October November Dece
    2016 2017 2018 2019 2020
    • Investing in Kotlin/MPP as the future
    • Server starts driving more logic on the client

    View Slide

  17. ay June July August September October November Dece
    2016 2017 2018 2019 2020
    • Investing in Kotlin/MPP as the future
    • Server starts driving more logic on the client
    • SQLDelight goes multiplatform (Droidcon talk)

    View Slide

  18. 2016 2017 2018 2019 2020
    ay June July August September October November Dece
    • Internal FormBlocker library

    View Slide

  19. 2016 2017 2018 2019 2020
    ay June July August September October November Dece
    • Internal FormBlocker library
    • Form-like UI spec implemented on each platform

    View Slide

  20. 2016 2017 2018 2019 2020
    ay June July August September October November Dece
    • Internal FormBlocker library
    • Form-like UI spec implemented on each platform
    • Web 1.0-like via protos (HTML) and native UI (browser)

    View Slide

  21. 2016 2017 2018 2019 2020
    ay June July August September October November Dece
    • Internal FormBlocker library
    • Form-like UI spec implemented on each platform
    • Web 1.0-like via protos (HTML) and native UI (browser)
    • Logic & controls baked in app

    View Slide

  22. 2018 2019 2020 2021 2022
    ay June July August September October November Dece
    • FormBlocker 2.0?

    View Slide

  23. 2018 2019 2020 2021 2022
    ay June July August September October November Dece
    • FormBlocker 2.0?
    • Share more than just protos with Kotlin mulitiplatform

    View Slide

  24. 2018 2019 2020 2021 2022
    ay June July August September October November Dece
    • FormBlocker 2.0?
    • Share more than just protos with Kotlin mulitiplatform
    • Web 2.0-like with rich, updatable logic

    View Slide

  25. 2018 2019 2020 2021 2022
    ay June July August September October November Dece
    • FormBlocker 2.0?
    • Share more than just protos with Kotlin mulitiplatform
    • Web 2.0-like with rich, updatable logic
    • Not abandon our native engineers

    View Slide

  26. 2018 2019 2020 2021 2022
    ay June July August September October November Dece
    • FormBlocker 2.0?
    • Share more than just protos with Kotlin mulitiplatform
    • Web 2.0-like with rich, updatable logic
    • Not abandon our native engineers
    • Evaluated the existing cross-platform solutions

    View Slide

  27. Figma Android Eng
    iOS Eng
    Web Eng

    View Slide

  28. Figma Android Eng
    iOS Eng
    Web Eng
    View Impl
    UIKit Impl
    DOM Impl

    View Slide

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

    View Slide

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

    View Slide

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

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

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

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

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

  41. Schema
    Widgets
    Compose

    View Slide

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

    View Slide

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

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

    View Slide

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

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

    View Slide

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

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

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

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    }

    View Slide

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

    }

    View Slide

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

    }

    View Slide

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

    }

    View Slide

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

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

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

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  76. Root FrameLayout

    View Slide

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

    View Slide

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

    View Slide

  79. Root
    Row
    Spacer
    Image Column
    Text Spacer Surface
    Text






    View Slide

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

    View Slide

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

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

    View Slide

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

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

    View Slide

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

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

    View Slide

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

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

    View Slide

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

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

    View Slide

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

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

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

    View Slide

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

    View Slide

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

    View Slide

  91. Demo

    View Slide

  92. View Slide

  93. class WorldClockService {
    fun formatTime(location: Location): String {

    }
    }

    View Slide

  94. class RealWorldClockService : WorldClockService {
    override fun formatTime(location: Location): String {

    }
    }
    interface WorldClockService : ZiplineService {
    fun formatTime(location: Location): String
    }
    WorldClockService

    View Slide

  95. class RealWorldClockService : WorldClockService {
    override fun formatTime(location: Location): String {

    }
    }
    zipline.bind(RealWorldClockService())
    interface : ZiplineService {
    fun formatTime(location: Location): String
    }
    val worldClock = zipline.take()
    println(worldClock.formatTime(Location("Droidcon NYC!"))

    View Slide

  96. interface : ZiplineService {
    fun formatTime(location: Location): String
    }
    val worldClock = zipline.take()
    println(worldClock.formatTime(Location("Droidcon NYC!"))

    View Slide

  97. interface : ZiplineService {
    fun formatTime(location: Location): String
    }
    val worldClock = zipline.take()
    println(worldClock.formatTime(Location("Droidcon NYC!"))
    class RealWorldClockService : WorldClockService {
    override fun formatTime(location: Location): String {
    Fancy new implementation…
    }
    }
    zipline.bind(RealWorldClockService())

    View Slide

  98. Schema
    Widgets
    Compose

    View Slide

  99. Schema
    Widgets
    Compose
    Compose

    Protocol
    Widget

    Protocol

    View Slide

  100. Widgets
    Widget

    Protocol
    Zipline
    Presenter
    Platform UI
    Compose
    Compose

    Protocol

    View Slide

  101. Widgets
    Widget

    Protocol
    Zipline
    Platform UI

    View Slide

  102. Widgets
    Widget

    Protocol
    Zipline
    Platform UI
    Presenter Compose
    Compose

    Protocol

    View Slide

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

    View Slide

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

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

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

  107. Redwood + Zipline ➡ Treehouse

    View Slide

  108. Widgets
    Widget

    Protocol
    Zipline
    Platform UI
    Presenter Compose
    Compose

    Protocol

    View Slide

  109. Treehouse
    Widgets
    Widget

    Protocol
    Zipline
    Platform UI
    Presenter Compose
    Compose

    Protocol

    View Slide

  110. Demo

    View Slide

  111. Modifiers
    @Composable
    private fun Greeting(name: String) {
    Column(modifier = Modifier.padding(24.dp)) {
    Text(text = "Hello,")
    Text(text = name)
    }
    }

    View Slide

  112. Modifiers
    @Composable
    private fun Greeting(name: String) {
    Column(modifier = Modifier.padding(24.dp)) {
    Text(text = "Hello,")
    Text(text = name)
    }
    }

    View Slide

  113. Modifiers
    @Composable
    private fun Greeting(name: String) {
    Column(modifier = Modifier.padding(24.dp)) {
    Text(text = "Hello,")
    Text(text = name)
    }
    }

    View Slide

  114. Modifiers
    @Widget(1)
    data class Column(
    @Children(1) val children: () -> Unit,
    )

    View Slide

  115. Modifiers
    @Widget(1)
    data class Column(
    @Property(1) val padding: Int = 24,
    @Children(1) val children: () -> Unit,
    )

    View Slide

  116. Modifiers
    Int 24
    @Widget(1)
    data class Column(
    @Property(1) val padding: Padding = Normal,
    @Children(1) val children: () -> Unit,
    ) {
    enum class Padding {
    Small,
    Normal,
    Large,
    }
    }

    View Slide

  117. Modifiers
    @Composable
    private fun Greeting(name: String) {
    Column(modifier = Modifier.padding(24.dp)) {
    Text(text = "Hello,")
    Text(text = name)
    }
    }

    View Slide

  118. Modifiers
    @Composable
    private fun Greeting(name: String) {
    Column {
    Text(text = "Hello,")
    Text(text = name)
    }
    }

    View Slide

  119. Modifiers
    @Composable
    private fun Greeting(name: String) {
    Column(padding = Large) {
    Text(text = "Hello,")
    Text(text = name)
    }
    }

    View Slide

  120. Modifiers
    @Composable
    private fun Greeting(name: String) {
    Column(modifier = Modifier.padding(24.dp)) {
    Text(text = "Hello,")
    Text(text = name)
    }
    }

    View Slide

  121. Modifiers
    @Composable
    private fun Greeting(name: String) {
    Column(modifier = Modifier.padding(24.dp)) {
    Text(text = "Hello,")
    Text(text = name)
    }
    }

    View Slide

  122. Modifiers
    @Composable
    private fun Greeting(name: String) {
    Column(modifier = Modifier.padding(24.dp)) {
    Text(
    text = "Hello,",
    modifier = Modifier.align(CenterHorizontally),
    )
    Text(text = name)
    }
    }

    View Slide

  123. Modifiers
    @Composable
    private fun Greeting(name: String) {
    Column(modifier = Modifier.padding(24.dp)) {
    Text(
    text = "Hello,",
    modifier = Modifier.align(CenterHorizontally),
    )
    Text(text = name)
    }
    }

    View Slide

  124. C
    o
    l
    u
    m
    n
    Modifiers
    @Composable
    private fun Greeting(name: String) {
    Row(modifier = Modifier.padding(24.dp)) {
    Text(
    text = "Hello,",
    modifier = Modifier.align(CenterHorizontally),
    )
    Text(text = name)
    }
    }

    View Slide

  125. Modifiers
    @Composable
    fun Column(
    modifier: Modifier = Modifier,
    content: @Composable ColumnScope.() -> Unit,
    ) { … }

    View Slide

  126. Modifiers
    @Composable
    fun Column(
    modifier: Modifier = Modifier,
    content: @Composable ColumnScope.() -> Unit,
    ) { … }

    View Slide

  127. Modifiers
    @Composable
    fun Column(
    modifier: Modifier = Modifier,
    content: @Composable ColumnScope.() -> Unit,
    ) { … }
    interface ColumnScope {
    fun Modifier.align(alignment: Alignment.Horizontal) { … }
    }

    View Slide

  128. Modifiers
    @Composable
    fun Column(
    modifier: Modifier = Modifier,
    content: @Composable ColumnScope.() -> Unit,
    ) { … }
    interface ColumnScope {
    fun Modifier.align(alignment: Alignment.Horizontal) { … }
    }

    View Slide

  129. Modifiers
    @Composable
    fun Column(
    modifier: Modifier = Modifier,
    content: @Composable ColumnScope.() -> Unit,
    ) { … }
    interface ColumnScope {
    fun Modifier.align(alignment: Alignment.Horizontal) { … }
    }
    val CenterHorizontally: Horizontal = …

    View Slide

  130. Modifiers
    @Widget(1)
    data class Column(
    @Property(1) val padding: Padding = Normal,
    @Children(1) val children: () -> Unit,
    ) {
    enum class Padding { Small, Normal, Large }
    }

    View Slide

  131. Modifiers
    @Widget(1)
    data class Column(
    @Property(1) val padding: Padding = Normal,
    @Children(1) val children: () -> Unit,
    ) {
    enum class Padding { Small, Normal, Large }
    }
    object ColumnScope

    View Slide

  132. Modifiers
    @Widget(1)
    data class Column(
    @Property(1) val padding: Padding = Normal,
    @Children(1) val children: ColumnScope.() -> Unit,
    ) {
    enum class Padding { Small, Normal, Large }
    }
    object ColumnScope

    View Slide

  133. Modifiers
    @Composable
    public fun Column(
    children: @Composable () -> Unit,
    ) { … }

    View Slide

  134. Modifiers
    @Composable
    public fun Column(
    children: @Composable () -> Unit,
    ) { … }
    object ColumnScope

    View Slide

  135. Modifiers
    @Composable
    public fun Column(
    children: @Composable ColumnScope.() -> Unit,
    ) { … }
    object ColumnScope

    View Slide

  136. Modifiers
    @Widget(1)
    data class Column(…) { … }
    object ColumnScope

    View Slide

  137. Modifiers
    @Widget(1)
    data class Column(…) { … }
    object ColumnScope
    public data class HorizontalAlignment(
    val align: Horizontal,
    )

    View Slide

  138. Modifiers
    @Widget(1)
    data class Column(…) { … }
    object ColumnScope
    @LayoutModifier(1)
    public data class HorizontalAlignment(
    val align: Horizontal,
    )

    View Slide

  139. Modifiers
    @Widget(1)
    data class Column(…) { … }
    object ColumnScope
    @LayoutModifier(1, ColumnScope::class)
    public data class HorizontalAlignment(
    val align: Horizontal,
    )

    View Slide

  140. Modifiers
    @Composable
    public fun Column(
    children: @Composable ColumnScope.() -> Unit,
    ) { … }
    object ColumnScope

    View Slide

  141. Modifiers
    @Composable
    public fun Column(
    children: @Composable ColumnScope.() -> Unit,
    ) { … }
    object ColumnScope {
    fun LayoutModifier.horizontalAlignment(
    align: Horizontal,
    ): LayoutModifier {

    }
    }

    View Slide

  142. Modifiers
    @Composable
    private fun Greeting(name: String) {
    Column(padding = Large) {
    Text(text = "Hello,")
    Text(text = name)
    }
    }

    View Slide

  143. Modifiers
    @Composable
    private fun Greeting(name: String) {
    Column(padding = Large) {
    Text(
    text = "Hello,",
    layoutModifier =
    LayoutModifier.horizontalAlignment(Center),
    )
    Text(text = name)
    }
    }

    View Slide

  144. Modifiers
    @Composable
    private fun Greeting(name: String) {
    Column(padding = Large) {
    Text(
    text = "Hello,",
    layoutModifier =
    LayoutModifier.horizontalAlignment(Center),
    )
    Text(text = name)
    }
    }

    View Slide

  145. Modifiers
    @Composable
    private fun Greeting(name: String) {
    Column(padding = Large) {
    Text(
    text = "Hello,",
    layoutModifier =
    LayoutModifier.horizontalAlignment(Center),
    )
    Text(text = name)
    }
    }

    View Slide

  146. C
    o
    l
    u
    m
    n
    Modifiers
    @Composable
    private fun Greeting(name: String) {
    Row(padding = Large) {
    Text(
    text = "Hello,",
    layoutModifier =
    LayoutModifier.horizontalAlignment(Center),
    )
    Text(text = name)
    }
    }

    View Slide

  147. Modifiers
    class AndroidText(
    override val value: TextView,
    ) : Text {
    override fun text(text: String?) {
    value.text = text
    }
    }

    View Slide

  148. Modifiers
    class AndroidText(
    override val value: TextView,
    ) : Text {
    override var layoutModifiers = LayoutModifier
    override fun text(text: String?) {
    value.text = text
    }
    }

    View Slide

  149. Modifiers
    class AndroidText(
    override val value: TextView,
    ) : Text {
    override var layoutModifiers = LayoutModifier
    set(layoutModifiers) {
    // Write data to value.layoutParams…
    field = layoutModifiers
    }
    override fun text(text: String?) {
    value.text = text
    }
    }

    View Slide

  150. Schema
    Widgets
    Compose
    Compose

    Protocol
    Widget

    Protocol

    View Slide

  151. Schema
    Widgets
    Compose
    Compose

    Protocol
    Widget

    Protocol
    Layout
    Modifiers

    View Slide

  152. Multiple children
    @Widget(1)
    data class Column(
    @Children(1) val children: () -> Unit,
    )

    View Slide

  153. Multiple children

    View Slide

  154. Multiple children

    View Slide

  155. Multiple children
    @Widget(6)
    data class Toolbar(
    @Property(1) val title: String,
    )

    View Slide

  156. Multiple children
    @Widget(6)
    data class Toolbar(
    @Property(1) val title: String,
    @Children(1) val start: () -> Unit,
    @Children(2) val end: () -> Unit,
    )

    View Slide

  157. Multiple children
    @Composable
    fun Toolbar(
    title: String,
    start: @Composable () -> Unit,
    end: @Composable () -> Unit,
    ) { … }

    View Slide

  158. Multiple children
    @Composable
    fun Toolbar(
    title: String,
    start: @Composable () -> Unit,
    end: @Composable () -> Unit,
    ) {
    Row {
    start()
    Text(title)
    end()
    }
    }

    View Slide

  159. Multiple children
    @Composable
    fun Toolbar(
    title: String,
    start: @Composable () -> Unit,
    end: @Composable () -> Unit,
    ) {
    Row {
    start()
    Text(title)
    end()
    }
    }

    View Slide

  160. Multiple children
    RedwoodComposeNode<…> {
    factory = EmojiWidgetFactory<*>::Toolbar
    updater = { set(title) { title(title) } }
    children = {}
    }
    @Composable
    fun Toolbar(
    title: String,
    start: @Composable () -> Unit,
    end: @Composable () -> Unit,
    ) {
    }

    View Slide

  161. Multiple children
    @Composable
    fun Toolbar(
    title: String,
    start: @Composable () -> Unit,
    end: @Composable () -> Unit,
    ) {
    RedwoodComposeNode<…> {
    factory = EmojiWidgetFactory<*>::Toolbar
    updater = { set(title) { title(title) } }
    children = {
    start()
    end()
    }
    }
    }

    View Slide

  162. Multiple children
    @Composable
    fun Toolbar(
    title: String,
    start: @Composable () -> Unit,
    end: @Composable () -> Unit,
    ) {
    RedwoodComposeNode<…> {
    factory = EmojiWidgetFactory<*>::Toolbar
    updater = { set(title) { title(title) } }
    children = {
    SyntheticChildren(1) { start() }
    SyntheticChildren(2) { end() }
    }
    }
    }

    View Slide

  163. Multiple children
    class AndroidToolbar(
    private val toolbarBinding: ToolbarBinding
    ) : Toolbar {
    override val start = ViewGroupChildren(toolbarBinding.start)
    override val end = ViewGroupChildren(toolbarBinding.start)
    fun title(title: String) {
    toolbarBinding.title.text = title
    }
    }

    View Slide

  164. Root FrameLayout

    View Slide

  165. Root
    Toolbar
    FrameLayout
    LinearLayout
    LinearLayout TextView LinearLayout

    View Slide

  166. Root
    Toolbar
    FrameLayout
    LinearLayout
    LinearLayout TextView LinearLayout
    Image

    View Slide

  167. Root
    Toolbar
    FrameLayout
    LinearLayout
    LinearLayout TextView LinearLayout
    Image

    View Slide

  168. Root
    Toolbar
    FrameLayout
    LinearLayout
    LinearLayout TextView LinearLayout
    Image
    ImageView

    View Slide

  169. Root
    Toolbar
    FrameLayout
    LinearLayout
    LinearLayout TextView LinearLayout
    Image Image
    ImageView

    View Slide

  170. Root
    Toolbar
    FrameLayout
    LinearLayout
    LinearLayout TextView LinearLayout
    Image Image
    ImageView

    View Slide

  171. Root
    Toolbar
    FrameLayout
    LinearLayout
    LinearLayout TextView LinearLayout
    Image Image
    ImageView ImageView

    View Slide

  172. Root
    Toolbar
    FrameLayout
    LinearLayout
    LinearLayout TextView LinearLayout
    Text
    Image Image
    ImageView ImageView

    View Slide

  173. Root
    Toolbar
    FrameLayout
    LinearLayout
    LinearLayout TextView LinearLayout
    Text
    Image Image
    ImageView TextView ImageView

    View Slide

  174. What's next?
    • Lint API check

    View Slide

  175. Lint API check
    @Widget(3) // Shipped in app v1.0
    data class Text(
    @Property(1) val text: String?,
    )

    View Slide

  176. Lint API check
    @Widget(3) // Shipped in app v1.0
    data class Text(
    @Property(1) val text: String?,
    )
    @Widget(4) // Shipped in app v1.2
    data class FancyText(
    @Property(1) val text: String?,
    )

    View Slide

  177. Lint API check
    @Composable fun Prompt(title: String) {
    Column {
    FancyText("Send money?")
    Row {
    Button("Send", onClick = { … })
    Button("Cancel", onClick = { … })
    }
    }
    }

    View Slide

  178. Lint API check
    @Composable fun Prompt(title: String) {
    Column {
    FancyText("Send money?")
    Row {
    Button("Send", onClick = { … })
    Button("Cancel", onClick = { … })
    }
    }
    }
    !
    !

    View Slide

  179. Lint API check
    @Composable fun Prompt(title: String) {
    Column {
    FancyText("Send money?")
    Row {
    Button("Send", onClick = { … })
    Button("Cancel", onClick = { … })
    }
    }
    }

    View Slide

  180. Lint API check
    @Composable fun Prompt(title: String) {
    Column {
    if (WidgetVersion >= VERSION_1_2) {
    FancyText("Send money?")
    } else {
    Text("Send money?")
    }
    Row {
    Button("Send", onClick = { … })
    Button("Cancel", onClick = { … })
    }
    }
    }

    View Slide

  181. Lint API check
    @Composable fun Prompt(title: String) {
    Column {
    if (WidgetVersion >= VERSION_1_2) {
    FancyText("Send money?")
    } else {
    Text("Send money?")
    }
    Row {
    Button("Send", onClick = { … })
    Button("Cancel", onClick = { … })
    }
    }
    }

    View Slide

  182. Lint API check
    @Composable fun Prompt(title: String) {
    Column {
    if (WidgetVersion >= VERSION_1_2) {
    FancyText("Send money?")
    } else {
    Text("Send money?")
    }
    Row {
    Button("Send", onClick = { … })
    Button("Cancel", onClick = { … })
    }
    }
    }

    View Slide

  183. What's next?
    • Lint API check

    • Optional basic row/column/box layout

    View Slide

  184. Optional basic row/column/box layout
    data class Column(
    val children: () -> Unit,
    )
    data class TextInput(
    val hint: String,
    val text: String,
    val onTextChanged: (String) -> Unit,
    )

    View Slide

  185. Optional basic row/column/box layout
    data class Column(
    val children: () -> Unit,
    )
    data class TextInput(
    val hint: String,
    val text: String,
    val onTextChanged: (String) -> Unit,
    )

    View Slide

  186. Optional basic row/column/box layout
    data class Column(
    val children: () -> Unit,
    )
    data class Row(
    val children: () -> Unit,
    )
    data class Box(
    val children: () -> Unit,
    )

    View Slide

  187. What's next?
    • Lint API check

    • Optional basic row/column/box layout

    • Optional paginated column

    View Slide

  188. Optional paginated column
    ScrollableColumn {
    for (emoji in emojis) {
    Row {
    Image(url = emoji.url)
    Text(text = emoji.name)
    }
    }
    }

    View Slide

  189. Optional paginated column
    LazyColumn {
    items(emojis) {
    Row {
    Image(url = emoji.url)
    Text(text = emoji.name)
    }
    }
    }
    S
    c
    r
    o
    l
    l
    a
    b
    l
    e
    f
    o
    r (
    e
    m
    o
    j
    i i
    n

    View Slide

  190. What's next?
    • Lint API check

    • Optional basic row/column/box layout

    • Optional paginated column

    • Testing API

    View Slide

  191. What's next?
    • Lint API check
    • Optional basic row/column/box layout
    • Optional paginated column
    • Testing API
    • Developer tooling

    View Slide

  192. What's next?
    • Lint API check
    • Optional basic row/column/box layout
    • Optional paginated column
    • Testing API
    • Developer tooling
    • Documentation "

    View Slide

  193. Should you use it?
    Not yet
    (probably)

    View Slide

  194. .com/cashapp/redwood

    View Slide

  195. @jakewharton
    @jessewilson
    Native UI with
    Multiplatform Compose

    View Slide