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

Talk given at Droidcon New York on September 15th, 2023, about how to leverage a design system to build server driven UI.

Ahmed El-Helw

September 29, 2023
Tweet

More Decks by Ahmed El-Helw

Other Decks in Programming

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(modifier: Modifier) { Label(text = text, maxLines = maxLines) } }
  11. class LabelData( private val text: String, private val maxLines: Int

    ) : Component { @Composable override fun Content(modifier: Modifier) { 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.fields %} let {{ key }} : {{ value }} {% endfor %} }
  16. data class {{ widget.name }} ( {% for key, value

    in widget.fields %} 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(modifier: Modifier) { LazyColumn(modifier = modifier) { contents.forEach { item { it.Content(modifier) } } } } }
  19. class ListComponent( private val contents: List<Component> ) : Component {

    @Composable override fun Content(modifier: Modifier) { LazyColumn(modifier = modifier) { contents.forEach { item { it.Content(modifier) } } } } }
  20. @Composable public fun Sample( response: ServerDrivenUiResponse, modifier: Modifier = Modifier

    ) { SampleTheme { Surface(modifier = modifier) { ServerDrivenUi( response, Modifier.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(modifier: Modifier) { Label( text = text, maxLines = maxLines, modifier = modifier.handleAction(actions) ) }
  23. // Internal API internal fun Modifier.handleActions(actions: List<Action>): Modifier = composed

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

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

    { val handler = LocalServerDrivenUiActionHandler.current val scope = rememberCoroutineScope() var localModifier = this actions.forEach { action -> localModifier = when (action) { is OnClick -> localModifier.clickable { scope.launch { handler.onClick(action) } } }
  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": [],

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

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

    "modifiers": [] }, { "type": "progressStatus", "amount": 3, "total": 10 } ], "modifiers": []
  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.