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

Bootstrapping Simple Server Driven UI from a De...

Bootstrapping Simple Server Driven UI from a Design System

In this talk, we will dive into the concept of using a design system to bootstrap a server-driven UI implementation using Compose on Android and SwiftUI on iOS. We will explore the benefits of bootstrapping SDUi from a design system. We'll discuss the advantages and limitations of this approach.

In addition, we'll also talk about how Kotlin Multiplatform can be used to build some interesting tooling around server-driven UI. We'll explore some examples of such tooling and discuss how it can help in streamlining the development process.

Abdulahi Osoble

September 17, 2023
Tweet

More Decks by Abdulahi Osoble

Other Decks in Technology

Transcript

  1. Problems • Iteration speed is critical for business success •

    Mobile releases take time for adoption • Consistency on iOS and Android
  2. data class LabelData(val text: String, val maxLines: Int) @Composable fun

    Label(data: LabelData) { Text(text = data.text, maxLines = data.maxLines) }
  3. This is a lot of Work • We’d have to

    expose: • Text size • Colors • Typefaces • Font weights • This also starts to tie us to the underlying platform
  4. Design System • Opinionated • Limits the landscape of what

    is possible • Lets us use larger building blocks • Allows us to save time
  5. Components • May not be easy to serialize / deserialize

    directly • Need a data representation
  6. Step 1 - Decide data representation of our widgets Step

    2 - Render that data representation
  7. data class LabelData(val text: String, val maxLines: Int) @Composable fun

    Label(data: LabelData) { Text(text = data.text, maxLines = data.maxLines) }
  8. How do we Render? @Composable fun render(component: Component) { when

    (component) { is LabelData -> Label(component) is ButtonData -> Button(component) // ... 
 } }
  9. How do we Render? @Composable fun render(component: Component) { when

    (component) { is LabelData -> Label(component) is ButtonData -> Button(component) // ... 
 } }
  10. class LabelData( private val text: String, private val maxLines: Int

    ) : Component { @Composable override fun Content(modi fi er: Modi fi er) { Label(text = text, maxLines = maxLines) } }
  11. class LabelData( private val text: String, private val maxLines: Int

    ) : Component { @Composable override fun Content(modi fi er: Modi fi er) { Label(text = text, maxLines = maxLines) } }
  12. iOS 16 class WidgetData: ObservableObject { @Published var count =

    0 } struct Widget: View { @ObservedObject var data: WidgetData var body: some View { /* ... */ } }
  13. iOS 17 - Observation Framework @Observable class WidgetData { var

    count = 0 } struct Widget: View { var data: WidgetData var body: some View { /* ... */ } }
  14. Other Options • Share models with KMP with a Swift

    wrapper • codegen Kotlin and Swift models
  15. import SwiftUI struct {{widget.name}} { {% for key, value in

    widget. fi elds %} let {{key}}: {{value}} {% endfor %} }
  16. data class {{widget.name}}( {% for key, value in widget. fi

    elds %} val {{key}}: {{value}}, {% endfor %} )
  17. Other Options • Share models with KMP with a Swift

    wrapper • codegen Kotlin and Swift models • Expect/action chicanery
  18. class ListComponent( private val contents: List<Component> ) : Component {

    @Composable override fun Content(modi fi er: Modi fi er) { LazyColumn(modi fi er = modi fi er) { contents.forEach { item { it.Content(modi fi er) } } } } }
  19. class ListComponent( private val contents: List<Component> ) : Component {

    @Composable override fun Content(modi fi er: Modi fi er) { LazyColumn(modi fi er = modi fi er) { contents.forEach { item { it.Content(modi fi er) } } } } }
  20. @Composable public fun Sample( response: ServerDrivenUiResponse, modi fi er: Modi

    fi er = Modi fi er ) { SampleTheme { Surface(modi fi er = modi fi er) { ServerDrivenUi( response, Modi fi er.padding(all = 8.dp) ) } } }
  21. // Public API interface ActionHandler { suspend fun onClick(action: OnClick)

    {} } val LocalServerDrivenUiActionHandler = staticCompositionLocalOf<ActionHandler> { error("nothing provided") }
  22. class LabelData( private val text: String, private val maxLines: Int,

    private val actions: List<Action> = emptyList() ) : Component { @Composable override fun Content(modi fi er: Modi fi er) { Label( text = text, maxLines = maxLines, modi fi er = modi fi er.handleAction(actions) ) } }
  23. // Internal API internal fun Modi fi er.handleActions(actions: List<Action>): Modi

    fi er = composed { val handler = LocalServerDrivenUiActionHandler.current val scope = rememberCoroutineScope() var localModi fi er = this actions.forEach { action -> localModi fi er = when (action) { is OnClick -> localModi fi er.clickable { scope.launch { handler.onClick(action) } } } } localModi fi er }
  24. // Internal API internal fun Modi fi er.handleActions(actions: List<Action>): Modi

    fi er = composed { val handler = LocalServerDrivenUiActionHandler.current val scope = rememberCoroutineScope() var localModi fi er = this actions.forEach { action -> localModi fi er = when (action) { is OnClick -> localModi fi er.clickable { scope.launch { handler.onClick(action) } } } } localModi fi er }
  25. // Internal API internal fun Modi fi er.handleActions(actions: List<Action>): Modi

    fi er = composed { val handler = LocalServerDrivenUiActionHandler.current val scope = rememberCoroutineScope() var localModi fi er = this actions.forEach { action -> localModi fi er = when (action) { is OnClick -> localModi fi er.clickable { scope.launch { handler.onClick(action) } } } } localModi fi er }
  26. // Public API val actionHandler = object : ActionHandler {

    override suspend fun onClick(action: OnClick) { when (action) { is OnClick.Deeplink -> { // .... } } } }
  27. // Public API val actionHandler = object : ActionHandler {

    override suspend fun onClick(action: OnClick) { when (action) { is OnClick.Deeplink -> { // .... } } } }
  28. Recap • Design system simplifies our scope and empowers SDUI

    • Actions are powerful and optional on all components • Components are serialized from payload and self render
  29. private val componentModule = SerializersModule { polymorphic(Component::class) { subclass(ListComponent::class) subclass(ListItemComponent::class)

    subclass(LabelComponent::class) // ... } } private val json = Json { serializersModule = componentModule }
  30. Open Deserialization • Pros: • Easy to add new components

    • Use SDUI without depending on design system
  31. Open Deserialization • Cons: • Easy to bypass design system

    • Duplication of widgets over time • Lose the ability to change without releasing a new version of the App
  32. { "type": "row", "contents": [ { "type": "column", "contents": [],

    "modi fi ers": [] }, { "type": "progressStatus", "amount": 3, "total": 10
 } ], "modi fi ers": [] }
  33. { "type": "row", "contents": [ { "type": "column", "contents": [],

    "modi fi ers": [] }, { "type": "progressStatus", "amount": 3, "total": 10
 } ], "modi fi ers": [] }
  34. { "type": "row", "contents": [ { "type": "column", "contents": [],

    "modi fi ers": [] }, { "type": "progressStatus", "amount": 3, "total": 10
 } ], "modi fi ers": [] }
  35. Open up Primitives • Pros: • Can only build on

    top of existing components • Can share these on the backend • Can update these without a new app update
  36. Open up Primitives • Cons: • More difficult to add

    components • May need to add more low level components
  37. Other Ideas • Web is useful for previewing SDUI from

    a CMS • Drag and drop tool or other generator • ChatGPT
  38. To help shipping speeds really fly, We started using server

    driven ui, building it against our system of design, colors, fonts, and the width of each line. We agreed on data representations, approved by Android and iOS nations. We followed it with a component interface, allowing us to deserialize a polymorphic base. We realized we'd have to glean, info on the type to draw on the screen. We need to be sure iOS and Android are in sync, as time goes on, we continue to think, do we generate code from a common source, or misuse expect actual with a bit of remorse, or do we just share models in KMP, this is yet another point in our decision tree. In order to support actions like click, without code that makes us sick, we have each action containing its data, handled by a composition local sometime later. How do we allow people to add new components? Multiple solutions, each with their proponents. Allow folks to define a component type, or implement row, column, and the low level hype. Since our design system is written in Compose, run it on multiple platforms we do propose, web and desktop make a lot of sense, open up nice doors while costing a mere pence. We hope you'll check out our GitHub repo, and make server driven ui your Home Depot.