Slide 1

Slide 1 text

Bootstrapping Simple Server Driven UI from a Design System Abdulahi Osoble Ahmed El-Helw

Slide 2

Slide 2 text

Problems • Iteration speed is critical for business success • Mobile releases take time for adoption • Consistency on iOS and Android

Slide 3

Slide 3 text

Today Mobile fetch data from APIs and bind them to UIs on the client side.

Slide 4

Slide 4 text

What If Our features come from an endpoint?

Slide 5

Slide 5 text

Server Driven UI Mobile fetches UI representation from APIs

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

Building SDUI

Slide 8

Slide 8 text

Building from Scratch • Need some UI components • Need a serializable data representation

Slide 9

Slide 9 text

{ "type": "label", "text": "Hello, World!" }

Slide 10

Slide 10 text

data class LabelData(val text: String) @Composable fun Label(data: LabelData) { Text(data.text) }

Slide 11

Slide 11 text

data class LabelData(val text: String) @Composable fun Label(data: LabelData) { Text(data.text) }

Slide 12

Slide 12 text

Let's add Max Lines

Slide 13

Slide 13 text

{ "type": "label", "text": "Hello, World! This is not very long ... ", "maxLines": 2 }

Slide 14

Slide 14 text

data class LabelData(val text: String, val maxLines: Int) @Composable fun Label(data: LabelData) { Text(text = data.text, maxLines = data.maxLines) }

Slide 15

Slide 15 text

Let's Add Typography?

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

Can we make the problem smaller?

Slide 18

Slide 18 text

Design System Already Solves this for Us

Slide 19

Slide 19 text

Design System • Opinionated • Limits the landscape of what is possible • Lets us use larger building blocks • Allows us to save time

Slide 20

Slide 20 text

Android • Views • Compose iOS • UIKit • SwiftUI

Slide 21

Slide 21 text

Design System • Components • Foundations (types and tokens)

Slide 22

Slide 22 text

Components • May not be easy to serialize / deserialize directly • Need a data representation

Slide 23

Slide 23 text

Types and Tokens • Styles, Color Tokens, etc • Easy to serialize / deserialize

Slide 24

Slide 24 text

Demo

Slide 25

Slide 25 text

Server Driven UI • Same payload should render on iOS and Android

Slide 26

Slide 26 text

Build a Serialization Layer

Slide 27

Slide 27 text

Step 1 - Decide data representation of our widgets Step 2 - Render that data representation

Slide 28

Slide 28 text

data class LabelData(val text: String, val maxLines: Int) @Composable fun Label(data: LabelData) { Text(text = data.text, maxLines = data.maxLines) }

Slide 29

Slide 29 text

No content

Slide 30

Slide 30 text

interface Component data class LabelData( val text: String, val maxLines: Int ): Component

Slide 31

Slide 31 text

interface Component data class LabelData( val text: String, val maxLines: Int ): Component

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

Can we Simplify? interface Component { @Composable fun Content(modifier: Modifier) }

Slide 35

Slide 35 text

class LabelData( private val text: String, private val maxLines: Int ) : Component { @Composable override fun Content(modifier: Modifier) { Label(text = text, maxLines = maxLines) } }

Slide 36

Slide 36 text

class LabelData( private val text: String, private val maxLines: Int ) : Component { @Composable override fun Content(modifier: Modifier) { Label(text = text, maxLines = maxLines) } }

Slide 37

Slide 37 text

@Composable fun Sample(component: Component, modifier: Modifier) { component.Content(modifier) }

Slide 38

Slide 38 text

Tradeoffs

Slide 39

Slide 39 text

Share with iOS or Not?

Slide 40

Slide 40 text

iOS 16 class WidgetData: ObservableObject { @Published var count = 0 } struct Widget: View { @ObservedObject var data: WidgetData var body: some View { /* ... */ } }

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

Other Options • Share models with KMP with a Swift wrapper • codegen Kotlin and Swift models

Slide 43

Slide 43 text

import SwiftUI struct {{ widget.name }} { {% for key, value in widget.fields %} let {{ key }} : {{ value }} {% endfor %} }

Slide 44

Slide 44 text

data class {{ widget.name }} ( {% for key, value in widget.fields %} val {{ key }} : {{ value }} , {% endfor %} )

Slide 45

Slide 45 text

Other Options • Share models with KMP with a Swift wrapper • codegen Kotlin and Swift models • Expect/action chicanery

Slide 46

Slide 46 text

expect interface Renderer

Slide 47

Slide 47 text

actual interface Renderer { @Composable fun Content(modifier: Modifier) }

Slide 48

Slide 48 text

actual interface Renderer

Slide 49

Slide 49 text

interface Component { fun renderer(): Renderer }

Slide 50

Slide 50 text

Public API

Slide 51

Slide 51 text

interface Component { @Composable fun Content(modifier: Modifier) }

Slide 52

Slide 52 text

data class ServerDrivenUiResponse( val root: Component )

Slide 53

Slide 53 text

@Composable fun ServerDrivenUi( response: ServerDrivenUiResponse, modifier: Modifier ) { response.root.Content(modifier) }

Slide 54

Slide 54 text

{ "root": { "type": "list", "contents": [ ] } } Example of List

Slide 55

Slide 55 text

class ListComponent( private val contents: List ) : Component { @Composable override fun Content(modifier: Modifier) { LazyColumn(modifier = modifier) { contents.forEach { item { it.Content(modifier) } } } } }

Slide 56

Slide 56 text

class ListComponent( private val contents: List ) : Component { @Composable override fun Content(modifier: Modifier) { LazyColumn(modifier = modifier) { contents.forEach { item { it.Content(modifier) } } } } }

Slide 57

Slide 57 text

How Do We Use This?

Slide 58

Slide 58 text

@Composable public fun Sample( response: ServerDrivenUiResponse, modifier: Modifier = Modifier ) { SampleTheme { Surface(modifier = modifier) { ServerDrivenUi( response, Modifier.padding(all = 8.dp) ) } } }

Slide 59

Slide 59 text

What about events?

Slide 60

Slide 60 text

Actions

Slide 61

Slide 61 text

• Based on user interaction • Broken down in sections • Must have common super-type

Slide 62

Slide 62 text

interface Action sealed class OnClick: Action { class Deeplink(val link: String): OnClick() }

Slide 63

Slide 63 text

interface Action sealed class OnClick: Action { class Deeplink(val link: String): OnClick() }

Slide 64

Slide 64 text

{ "actions": [ { "type": "onClick_deeplink", "link": “careem: // link“ } ] }

Slide 65

Slide 65 text

{ "actions": [ { "type": "onClick_deeplink", "link": “careem: // link“ } ] }

Slide 66

Slide 66 text

How do we handle Actions?

Slide 67

Slide 67 text

// Public API interface ActionHandler { suspend fun onClick(action: OnClick) {} } val LocalServerDrivenUiActionHandler = staticCompositionLocalOf { error("nothing provided") }

Slide 68

Slide 68 text

class LabelData( private val text: String, private val maxLines: Int, private val actions: List = emptyList() ) : Component { @Composable override fun Content(modifier: Modifier) { Label( text = text, maxLines = maxLines, modifier = modifier.handleAction(actions) ) }

Slide 69

Slide 69 text

// Internal API internal fun Modifier.handleActions(actions: List): 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) } } }

Slide 70

Slide 70 text

// Internal API internal fun Modifier.handleActions(actions: List): 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) } } }

Slide 71

Slide 71 text

// Internal API internal fun Modifier.handleActions(actions: List): 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) } } }

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

// Public API CompositionLocalProvider( LocalServerDrivenUiActionHandler provides actionHandler ) { . .. }

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

How about custom components?

Slide 77

Slide 77 text

No content

Slide 78

Slide 78 text

Extensibility

Slide 79

Slide 79 text

Open Deserialization

Slide 80

Slide 80 text

private val componentModule = SerializersModule { polymorphic(Component :: class) { subclass(ListComponent :: class) subclass(ListItemComponent :: class) subclass(LabelComponent :: class) // ... } } private val json = Json { serializersModule = componentModule }

Slide 81

Slide 81 text

Open Deserialization • Pros: • Easy to add new components • Use SDUI without depending on design system

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

Open up Primitives

Slide 84

Slide 84 text

Open up Primitives • Rows • Columns • Modifiers

Slide 85

Slide 85 text

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

Slide 86

Slide 86 text

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

Slide 87

Slide 87 text

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

Slide 88

Slide 88 text

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

Slide 89

Slide 89 text

Open up Primitives • Cons: • More difficult to add components • May need to add more low level components

Slide 90

Slide 90 text

Demo

Slide 91

Slide 91 text

Tooling

Slide 92

Slide 92 text

Other Ideas • Web is useful for previewing SDUI from a CMS • Drag and drop tool or other generator • ChatGPT

Slide 93

Slide 93 text

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.

Slide 94

Slide 94 text

github.com/ahmedre/sdui