Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Native UI with Multiplatform Compose (Droidcon NYC 2022)

Jake Wharton
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

September 01, 2022
Tweet

More Decks by Jake Wharton

Other Decks in Technology

Transcript

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

    Powered by Compose • Shared presenter logic across all platforms
  2. 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
  3. 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
  4. 2012 2013 2014 September October November Dece • Native UI

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

    feels best • Design system is cross platform • Hire the best engineers on each platform
  6. 2014 2015 2016 2017 2018 ay June July August September

    October November Dece • Code sharing is awesome
  7. 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
  8. 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)
  9. ay June July August September October November Dece 2016 2017

    2018 2019 2020 • Investing in Kotlin/MPP as the future
  10. 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
  11. 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)
  12. 2016 2017 2018 2019 2020 ay June July August September

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

    October November Dece • Internal FormBlocker library • Form-like UI spec implemented on each platform
  14. 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)
  15. 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
  16. 2018 2019 2020 2021 2022 ay June July August September

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

    October November Dece • FormBlocker 2.0? • Share more than just protos with Kotlin mulitiplatform
  18. 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
  19. 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
  20. 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
  21. Redwood Schema data class Column( val children: () -> Unit,

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

    val birthday: LocalDate, ) data class TextInput( val hint: String, val text: String, val onTextChanged: (String) -> Unit, )
  23. 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, )
  24. Redwood Schema data class Column( val children: () -> Unit,

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

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

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

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

    ) data class TextInput( val hint: String, val text: String, val onTextChanged: (String) -> Unit, )
  29. 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, )
  30. 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, )
  31. 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, )
  32. Redwood Compose data class Column( val children: () -> Unit,

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

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

    ) data class TextInput( val hint: String, val text: String, val onTextChanged: (String) -> Unit, )
  37. Redwood Widgets 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)?) }
  38. Redwood Widgets 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)?) }
  39. Redwood Widgets 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)?) }
  40. Redwood Widgets class ViewColumn : Column<View> interface Column<T : Any>

    : Widget<T> { val children: Widget.Children<T> }
  41. Redwood Widgets class ViewColumn( override val value: LinearLayout, ) :

    Column<View> interface Column<T : Any> : Widget<T> { val children: Widget.Children<T> }
  42. Redwood Widgets class ViewColumn( override val value: LinearLayout, ) :

    Column<View> { override val children = ViewGroupChildren(value) } interface Column<T : Any> : Widget<T> { val children: Widget.Children<T> }
  43. Redwood Widgets interface EmojiWidgetFactory<T : Any> : Widget.Factory<T> { fun

    Column(): Column<T> fun ScrollableColumn(): ScrollableColumn<T> fun TextInput(): TextInput<T> fun Image(): Image<T> }
  44. Redwood Widgets class EmojiViewWidgetFactory( private val context: Context, ) :

    EmojiWidgetFactory<View> interface EmojiWidgetFactory<T : Any> : Widget.Factory<T> { fun Column(): Column<T> … }
  45. Redwood Widgets class EmojiViewWidgetFactory( private val context: Context, ) :

    EmojiWidgetFactory<View> { override fun Column(): Column<View> { } } interface EmojiWidgetFactory<T : Any> : Widget.Factory<T> { fun Column(): Column<T> … }
  46. Redwood Widgets class EmojiViewWidgetFactory( private val context: Context, ) :

    EmojiWidgetFactory<View> { override fun Column(): Column<View> { return ViewColumn(LinearLayout(context)) } } interface EmojiWidgetFactory<T : Any> : Widget.Factory<T> { fun Column(): Column<T> … }
  47. Redwood Widgets class EmojiViewWidgetFactory( private val context: Context, ) :

    EmojiWidgetFactory<View> { override fun Column(): Column<View> { return ViewColumn(LinearLayout(context)) } … } interface EmojiWidgetFactory<T : Any> : Widget.Factory<T> { fun Column(): Column<T> … }
  48. @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, ) } } } }
  49. @Composable fun MessageCard(…) { Row(…) { Image(…) Spacer(…) Column {

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    <div> <div> <img> <div> <span> <div> <div> <span>
  66. Redwood Compose & Widgets @Composable fun TextInput( hint: String, text:

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

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

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

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

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

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

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

    String, onTextChanged: (String) -> Unit, ) { RedwoodComposeNode<EmojiWidgetFactory<*>, TextInput<*>>( factory = EmojiWidgetFactory<*>::TextInput, update = { set(hint) { hint(hint) } set(text) { text(text) } set(onTextChanged) { onTextChanged(onTextChanged) } }, ) }
  74. Root Row Spacer Image Column Text Spacer Surface Text FrameLayout

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

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

    LinearLayout View ImageView LinearLayout View FrameLayout TextView TextView Text(msg.body, …)
  77. class RealWorldClockService : WorldClockService { override fun formatTime(location: Location): String

    { … } } interface WorldClockService : ZiplineService { fun formatTime(location: Location): String } WorldClockService
  78. class RealWorldClockService : WorldClockService { override fun formatTime(location: Location): String

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

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

    worldClock = zipline.take<WorldClockService>() println(worldClock.formatTime(Location("Droidcon NYC!")) class RealWorldClockService : WorldClockService { override fun formatTime(location: Location): String { Fancy new implementation… } } zipline.bind<WorldClockService>(RealWorldClockService())
  81. Redwood Protocol data class Column( val children: () -> Unit,

    ) data class TextInput( val hint: String, val text: String, val onTextChanged: (String) -> Unit, )
  82. 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, )
  83. 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, )
  84. 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, )
  85. 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, } }
  86. Modifiers @Composable private fun Greeting(name: String) { Column(modifier = Modifier.padding(24.dp))

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

    { Text( text = "Hello,", modifier = Modifier.align(CenterHorizontally), ) Text(text = name) } }
  88. 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) } }
  89. Modifiers @Composable fun Column( modifier: Modifier = Modifier, content: @Composable

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

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

    ColumnScope.() -> Unit, ) { … } interface ColumnScope { fun Modifier.align(alignment: Alignment.Horizontal) { … } } val CenterHorizontally: Horizontal = …
  92. Modifiers @Widget(1) data class Column( @Property(1) val padding: Padding =

    Normal, @Children(1) val children: () -> Unit, ) { enum class Padding { Small, Normal, Large } }
  93. 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
  94. 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
  95. Modifiers @Widget(1) data class Column(…) { … } object ColumnScope

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

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

    @LayoutModifier(1, ColumnScope::class) public data class HorizontalAlignment( val align: Horizontal, )
  98. Modifiers @Composable public fun Column( children: @Composable ColumnScope.() -> Unit,

    ) { … } object ColumnScope { fun LayoutModifier.horizontalAlignment( align: Horizontal, ): LayoutModifier { … } }
  99. Modifiers @Composable private fun Greeting(name: String) { Column(padding = Large)

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

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

    { Text( text = "Hello,", layoutModifier = LayoutModifier.horizontalAlignment(Center), ) Text(text = name) } }
  102. 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) } }
  103. Modifiers class AndroidText( override val value: TextView, ) : Text<View>

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

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

    { override var layoutModifiers = LayoutModifier set(layoutModifiers) { // Write data to value.layoutParams… field = layoutModifiers } override fun text(text: String?) { value.text = text } }
  106. Multiple children @Widget(6) data class Toolbar( @Property(1) val title: String,

    @Children(1) val start: () -> Unit, @Children(2) val end: () -> Unit, )
  107. Multiple children @Composable fun Toolbar( title: String, start: @Composable ()

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

    -> Unit, end: @Composable () -> Unit, ) { Row { start() Text(title) end() } }
  109. Multiple children RedwoodComposeNode<…> { factory = EmojiWidgetFactory<*>::Toolbar updater = {

    set(title) { title(title) } } children = {} } @Composable fun Toolbar( title: String, start: @Composable () -> Unit, end: @Composable () -> Unit, ) { }
  110. 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() } } }
  111. 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() } } } }
  112. Multiple children class AndroidToolbar( private val toolbarBinding: ToolbarBinding ) :

    Toolbar<View> { override val start = ViewGroupChildren(toolbarBinding.start) override val end = ViewGroupChildren(toolbarBinding.start) fun title(title: String) { toolbarBinding.title.text = title } }
  113. Lint API check @Widget(3) // Shipped in app v1.0 data

    class Text( @Property(1) val text: String?, )
  114. 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?, )
  115. Lint API check @Composable fun Prompt(title: String) { Column {

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

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

    FancyText("Send money?") Row { Button("Send", onClick = { … }) Button("Cancel", onClick = { … }) } } }
  118. 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 = { … }) } } }
  119. 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 = { … }) } } }
  120. 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 = { … }) } } }
  121. 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, )
  122. 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, )
  123. Optional basic row/column/box layout data class Column( val children: ()

    -> Unit, ) data class Row( val children: () -> Unit, ) data class Box( val children: () -> Unit, )
  124. Optional paginated column ScrollableColumn { for (emoji in emojis) {

    Row { Image(url = emoji.url) Text(text = emoji.name) } } }
  125. 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
  126. What's next? • Lint API check • Optional basic row/column/box

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

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

    layout • Optional paginated column • Testing API • Developer tooling • Documentation "