Slide 1

Slide 1 text

No content

Slide 2

Slide 2 text

Rapidly iterating across platforms using server-driven UI Laura Kelly @heylaurakelly

Slide 3

Slide 3 text

• Android Engineer @ Airbnb • Previously front-end web • Trip Platform team • Powerlifter • Whiskey connoisseur @heylaurakelly Who I am

Slide 4

Slide 4 text

1. The problem: iterative, cross-platform products 2. Why server-driven UI fit the bill 3. Deep-dive into Android implementation 4. Case studies from Airbnb @heylaurakelly Outline

Slide 5

Slide 5 text

@heylaurakelly Airbnb is adding new products all the time Homes

Slide 6

Slide 6 text

@heylaurakelly Airbnb is adding new products all the time Homes Experiences

Slide 7

Slide 7 text

@heylaurakelly Airbnb is adding new products all the time Homes Experiences Restaurants

Slide 8

Slide 8 text

@heylaurakelly Airbnb is adding new products all the time Homes Experiences Restaurants Freeform Events

Slide 9

Slide 9 text

@heylaurakelly Homes Experiences Restaurants Coworking Spaces We were rebuilding nearly the same screen, multiplying our efforts across the codebase

Slide 10

Slide 10 text

@heylaurakelly

Slide 11

Slide 11 text

…and across platforms @heylaurakelly

Slide 12

Slide 12 text

There’s got to be a better way @heylaurakelly

Slide 13

Slide 13 text

WHAT WOULD AN IDEAL SYSTEM LOOK LIKE? @heylaurakelly

Slide 14

Slide 14 text

Easy to understand @heylaurakelly

Slide 15

Slide 15 text

Flexible to adapt to designers @heylaurakelly

Slide 16

Slide 16 text

Launch without a Play Store release @heylaurakelly

Slide 17

Slide 17 text

Minimize repetition @heylaurakelly

Slide 18

Slide 18 text

Easy to maintain @heylaurakelly

Slide 19

Slide 19 text

SERVER-DRIVEN UI @heylaurakelly

Slide 20

Slide 20 text

@heylaurakelly

Slide 21

Slide 21 text

Example API response @heylaurakelly {
 "type": "place",
 "id": "123",
 "rows": [ {
 "type": "row:carousel",
 "image_urls": [ "sightglassImage.png” ]
 },
 {
 "type": “row:title“,
 "title": "Sightglass Coffee",
 "starts_at": “Sun, Sept 24, 7:00 PM"
 },
 {
 "type": "row:action",
 "actions": [ /* ... actions here */ ]
 },
 {
 "type": "row:map",
 "address": "1234 Main Street"
 }
 ]
 }

Slide 22

Slide 22 text

{
 "type": "place",
 "id": "123",
 "rows": [ {
 "type": "row:carousel",
 "image_urls": [ "sightglassImage.png” ]
 },
 {
 "type": “row:title“,
 "title": "Sightglass Coffee",
 "starts_at": “Sun, Sept 24, 7:00 PM"
 },
 {
 "type": "row:action",
 "actions": [ /* ... actions here */ ]
 },
 {
 "type": "row:map",
 "address": "1234 Main Street"
 }
 ]
 } @heylaurakelly API Response

Slide 23

Slide 23 text

pol·y·mor·phism the provision of a single interface to entities of different types @heylaurakelly

Slide 24

Slide 24 text

LinkRow { id, type, deeplink } ActionRow { id, type, actions } CarouselRow { id, type, imageUrls} MapRow { id, type, lat, long } Row { id, type } @heylaurakelly

Slide 25

Slide 25 text

LinkRow { id, type, deeplink } ActionRow { id, type, actions } CarouselRow { id, type, imageUrls} MapRow { id, type, lat, long } Row { id, type } @heylaurakelly

Slide 26

Slide 26 text

LinkRow { id, type, deeplink } ActionRow { id, type, actions } CarouselRow { id, type, imageUrls} MapRow { id, type, lat, long } Row { id, type } @heylaurakelly

Slide 27

Slide 27 text

{
 "type": "place",
 "id": "123",
 "rows": [ {
 "type": "row:carousel",
 "image_urls":[ "sightglassImage.png” ]
 },
 {
 "type": “row:title“,
 "title": "Sightglass Coffee",
 "starts_at": “Sun, Sept 24, 7:00 PM"
 },
 {
 "type": "row:action",
 "actions": [ /* ... actions here */ ]
 } ...
 ]
 } API Response @heylaurakelly

Slide 28

Slide 28 text

{
 "type": "place",
 "id": "123",
 "rows": [ {
 "type": "row:carousel",
 "image_urls":[ "sightglassImage.png” ]
 },
 {
 "type": “row:title“,
 "title": "Sightglass Coffee",
 "starts_at": “Sun, Sept 24, 7:00 PM"
 },
 {
 "type": "row:action",
 "actions": [ /* ... actions here */ ]
 } ...
 ]
 } API Response @heylaurakelly @JsonSubTypes({ @JsonSubTypes.Type( value = CarouselDataModel.class, name = “row:carousel” ), @JsonSubTypes.Type( value = TitleRowDataModel.class, name = “row:title” ), @JsonSubTypes.Type( value = ActionRowDataModel.class, name = “row:action" ) . . .
 }) Map to Data Models

Slide 29

Slide 29 text

{
 "type": "place",
 "id": "123",
 "rows": [ {
 "type": "row:carousel",
 "image_urls":[ "sightglassImage.png” ]
 },
 {
 "type": “row:title“,
 "title": "Sightglass Coffee",
 "starts_at": “Sun, Sept 24, 7:00 PM"
 },
 {
 "type": "row:action",
 "actions": [ /* ... actions here */ ]
 } ...
 ]
 } API Response @heylaurakelly @JsonSubTypes({ @JsonSubTypes.Type( value = CarouselDataModel.class, name = “row:carousel” ), @JsonSubTypes.Type( value = TitleRowDataModel.class, name = “row:title” ), @JsonSubTypes.Type( value = ActionRowDataModel.class, name = “row:action" ) . . .
 }) Map to Data Models

Slide 30

Slide 30 text

{
 "type": "place",
 "id": "123",
 "rows": [ {
 "type": "row:carousel",
 "image_urls":[ "sightglassImage.png” ]
 },
 {
 "type": “row:title“,
 "title": "Sightglass Coffee",
 "starts_at": “Sun, Sept 24, 7:00 PM"
 },
 {
 "type": "row:action",
 "actions": [ /* ... actions here */ ]
 } ...
 ]
 } API Response @heylaurakelly @JsonSubTypes({ @JsonSubTypes.Type( value = CarouselDataModel.class, name = “row:carousel” ), @JsonSubTypes.Type( value = TitleRowDataModel.class, name = “row:title” ), @JsonSubTypes.Type( value = ActionRowDataModel.class, name = “row:action" ) . . .
 }) Map to Data Models

Slide 31

Slide 31 text

{
 "type": "place",
 "id": "123",
 "rows": [ {
 "type": "row:carousel",
 "image_urls":[ "sightglassImage.png” ]
 },
 {
 "type": “row:title“,
 "title": "Sightglass Coffee",
 "starts_at": “Sun, Sept 24, 7:00 PM"
 },
 {
 "type": "row:action",
 "actions": [ /* ... actions here */ ]
 } ...
 ]
 } API Response @heylaurakelly Frontend Web Code export default { 'row:carousel': CarouselRow, 'row:title': TitleRow, 'row:action': ActionRow, . . .
 }

Slide 32

Slide 32 text

@JsonSubTypes({ @JsonSubTypes.Type( value = CarouselDataModel.class, name = “row:carousel” ), @JsonSubTypes.Type( value = TitleRowDataModel.class, name = “row:title” ), @JsonSubTypes.Type( value = ActionRowDataModel.class, name = “row:action" ) . . .
 }) Map to Data Models {
 "type": "place",
 "id": "123",
 "rows": [ {
 "type": "row:carousel",
 "image_urls":[ "sightglassImage.png” ]
 },
 {
 "type": “row:title“,
 "title": "Sightglass Coffee",
 "starts_at": “Sun, Sept 24, 7:00 PM"
 },
 {
 "type": "row:action",
 "actions": [ /* ... actions here */ ]
 } ...
 ]
 } API Response UI Rendering @heylaurakelly

Slide 33

Slide 33 text

@JsonSubTypes({ @JsonSubTypes.Type( value = CarouselDataModel.class, name = “row:carousel” ), @JsonSubTypes.Type( value = TitleRowDataModel.class, name = “row:title” ), @JsonSubTypes.Type( value = ActionRowDataModel.class, name = “row:action" ) . . .
 }) Map to Data Models {
 "type": "place",
 "id": "123",
 "rows": [ {
 "type": "row:carousel",
 "image_urls":[ "sightglassImage.png” ]
 },
 {
 "type": “row:title“,
 "title": "Sightglass Coffee",
 "starts_at": “Sun, Sept 24, 7:00 PM"
 },
 {
 "type": "row:action",
 "actions": [ /* ... actions here */ ]
 } ...
 ]
 } API Response UI Rendering @heylaurakelly

Slide 34

Slide 34 text

@JsonSubTypes({ @JsonSubTypes.Type( value = CarouselDataModel.class, name = “row:carousel” ), @JsonSubTypes.Type( value = TitleRowDataModel.class, name = “row:title” ), @JsonSubTypes.Type( value = ActionRowDataModel.class, name = “row:action" ) . . .
 }) Map to Data Models {
 "type": "place",
 "id": "123",
 "rows": [ {
 "type": "row:carousel",
 "image_urls":[ "sightglassImage.png” ]
 },
 {
 "type": “row:title“,
 "title": "Sightglass Coffee",
 "starts_at": “Sun, Sept 24, 7:00 PM"
 },
 {
 "type": "row:action",
 "actions": [ /* ... actions here */ ]
 } ...
 ]
 } API Response UI Rendering @heylaurakelly

Slide 35

Slide 35 text

@JsonSubTypes({ @JsonSubTypes.Type( value = CarouselDataModel.class, name = “row:carousel” ), @JsonSubTypes.Type( value = TitleRowDataModel.class, name = “row:title” ), @JsonSubTypes.Type( value = ActionRowDataModel.class, name = “row:action" ) . . .
 }) Map to Data Models {
 "type": "place",
 "id": "123",
 "rows": [ {
 "type": "row:carousel",
 "image_urls":[ "sightglassImage.png” ]
 },
 {
 "type": “row:title“,
 "title": "Sightglass Coffee",
 "starts_at": “Sun, Sept 24, 7:00 PM"
 },
 {
 "type": "row:action",
 "actions": [ /* ... actions here */ ]
 } ...
 ]
 } API Response UI Rendering @heylaurakelly

Slide 36

Slide 36 text

@JsonSubTypes({ @JsonSubTypes.Type( value = CarouselDataModel.class, name = “row:carousel” ), @JsonSubTypes.Type( value = TitleRowDataModel.class, name = “row:title” ), @JsonSubTypes.Type( value = ActionRowDataModel.class, name = “row:action" ) . . .
 }) Map to Data Models {
 "type": "place",
 "id": "123",
 "rows": [ {
 "type": "row:carousel",
 "image_urls":[ "sightglassImage.png” ]
 },
 {
 "type": “row:title“,
 "title": "Sightglass Coffee",
 "starts_at": “Sun, Sept 24, 7:00 PM"
 },
 {
 "type": "row:action",
 "actions": [ /* ... actions here */ ]
 } ...
 ]
 } API Response UI Rendering @heylaurakelly Rendering Framework Carousel Title Row Action Row

Slide 37

Slide 37 text

@JsonSubTypes({ @JsonSubTypes.Type( value = CarouselDataModel.class, name = “row:carousel” ), @JsonSubTypes.Type( value = TitleRowDataModel.class, name = “row:title” ), @JsonSubTypes.Type( value = ActionRowDataModel.class, name = “row:action" ) . . .
 }) Map to Data Models {
 "type": "place",
 "id": "123",
 "rows": [ {
 "type": "row:carousel",
 "image_urls":[ "sightglassImage.png” ]
 },
 {
 "type": “row:title“,
 "title": "Sightglass Coffee",
 "starts_at": “Sun, Sept 24, 7:00 PM"
 },
 {
 "type": "row:action",
 "actions": [ /* ... actions here */ ]
 } ...
 ]
 } API Response UI Rendering @heylaurakelly Rendering Framework Title Row Action Row Map Row

Slide 38

Slide 38 text

@JsonSubTypes({ @JsonSubTypes.Type( value = CarouselDataModel.class, name = “row:carousel” ), @JsonSubTypes.Type( value = TitleRowDataModel.class, name = “row:title” ), @JsonSubTypes.Type( value = ActionRowDataModel.class, name = “row:action" ) . . .
 }) Map to Data Models {
 "type": "place",
 "id": "123",
 "rows": [ {
 "type": "row:carousel",
 "image_urls":[ "sightglassImage.png” ]
 },
 {
 "type": “row:title“,
 "title": "Sightglass Coffee",
 "starts_at": “Sun, Sept 24, 7:00 PM"
 },
 {
 "type": "row:action",
 "actions": [ /* ... actions here */ ]
 } ...
 ]
 } API Response UI Rendering @heylaurakelly Rendering Framework Title Row Action Row Map Row

Slide 39

Slide 39 text

BENEFITS WE SAW AFTER BUILDING THE SYSTEM @heylaurakelly

Slide 40

Slide 40 text

Build once @heylaurakelly

Slide 41

Slide 41 text

Easily accommodates changing minds @heylaurakelly

Slide 42

Slide 42 text

Reuse existing UI component libraries @heylaurakelly

Slide 43

Slide 43 text

Iterate, reconfigure, and experiment on our own time @heylaurakelly

Slide 44

Slide 44 text

ANDROID DEEP DIVE @heylaurakelly

Slide 45

Slide 45 text

JSON 1. JSON from server @heylaurakelly

Slide 46

Slide 46 text

JSON ArrayList rows( ) 1. JSON from server 2. Parse network response @heylaurakelly

Slide 47

Slide 47 text

JSON ArrayList rows( ) LinkRow MapRow TitleRow HostAvatarRow 1. JSON from server 2. Parse network response 3. Parse row subtypes @heylaurakelly

Slide 48

Slide 48 text

JSON ReservationController ReservationFragment ArrayList rows( ) LinkRow MapRow TitleRow HostAvatarRow 1. JSON from server 2. Parse network response 3. Parse row subtypes 4. Configure UI models @heylaurakelly

Slide 49

Slide 49 text

JSON ReservationController ReservationFragment ArrayList rows( ) LinkRow MapRow TitleRow HostAvatarRow 1. JSON from server 2. Parse network response 3. Parse row subtypes 4. Configure UI models 5. Render components @heylaurakelly

Slide 50

Slide 50 text

JSON ReservationController ReservationFragment ArrayList rows( ) LinkRow MapRow TitleRow HostAvatarRow 1. JSON from server 2. Parse network response 3. Parse row subtypes 4. Configure UI models 5. Render components @heylaurakelly

Slide 51

Slide 51 text

@AutoValue @JsonDeserialize(builder = AutoValue_Reservation.Builder.class) public abstract class Reservation { @JsonProperty public abstract String primary_key(); @JsonProperty public abstract ArrayList rows(); ... } 2. Parse network response @heylaurakelly

Slide 52

Slide 52 text

@AutoValue @JsonDeserialize(builder = AutoValue_Reservation.Builder.class) public abstract class Reservation { @JsonProperty public abstract String primary_key(); @JsonProperty public abstract ArrayList rows(); ... } 2. Parse network response @heylaurakelly

Slide 53

Slide 53 text

JSON ReservationController ReservationFragment ArrayList rows( ) LinkRow MapRow TitleRow HostAvatarRow 1. JSON from server 2. Parse network response 3. Parse row subtypes 4. Configure UI models 5. Render components @heylaurakelly

Slide 54

Slide 54 text

@heylaurakelly @JsonTypeInfo(...) @JsonSubTypes({ @JsonSubTypes.Type(value = LinkRowDataModel.class, name = "row:link"), @JsonSubTypes.Type(value = MapRowDataModel.class, name = "row:map"), @JsonSubTypes.Type(value = TitleRowDataModel.class, name = “row:title”), @JsonSubTypes.Type(value = HostAvatarRowDataModel.class, name = “row:host_avatar”) ... }) public interface RowDataModel extends Parcelable { ... } 3. Parse row subtypes

Slide 55

Slide 55 text

@heylaurakelly @JsonTypeInfo(...) @JsonSubTypes({ @JsonSubTypes.Type(value = LinkRowDataModel.class, name = "row:link"), @JsonSubTypes.Type(value = MapRowDataModel.class, name = "row:map"), @JsonSubTypes.Type(value = TitleRowDataModel.class, name = “row:title”), @JsonSubTypes.Type(value = HostAvatarRowDataModel.class, name = “row:host_avatar”) ... }) public interface RowDataModel extends Parcelable { ... } 3. Parse row subtypes

Slide 56

Slide 56 text

@JsonTypeInfo(...) @JsonSubTypes({ @JsonSubTypes.Type(value = LinkRowDataModel.class, name = "row:link"), @JsonSubTypes.Type(value = MapRowDataModel.class, name = "row:map"), @JsonSubTypes.Type(value = TitleRowDataModel.class, name = “row:title”), @JsonSubTypes.Type(value = HostAvatarRowDataModel.class, name = “row:host_avatar”) ... }) public interface RowDataModel extends Parcelable { ... } 3. Parse row subtypes @heylaurakelly

Slide 57

Slide 57 text

@JsonTypeInfo(...) @JsonSubTypes({ @JsonSubTypes.Type(value = LinkRowDataModel.class, name = "row:link"), @JsonSubTypes.Type(value = MapRowDataModel.class, name = "row:map"), @JsonSubTypes.Type(value = TitleRowDataModel.class, name = “row:title”), @JsonSubTypes.Type(value = HostAvatarRowDataModel.class, name = “row:host_avatar”) ... }) public interface RowDataModel extends Parcelable { ... } 3. Parse row subtypes @heylaurakelly

Slide 58

Slide 58 text

@AutoValue @JsonDeserialize(builder = AutoValue_LinkRowDataModel.Builder.class) @JsonTypeName("row:link") public abstract class LinkRowDataModel implements RowDataModel { @JsonProperty public abstract String id(); @JsonProperty public abstract String title(); @JsonProperty public abstract String app_url(); ... } 3. Parse row subtypes @heylaurakelly

Slide 59

Slide 59 text

@AutoValue @JsonDeserialize(builder = AutoValue_LinkRowDataModel.Builder.class) @JsonTypeName("row:link") public abstract class LinkRowDataModel implements RowDataModel { @JsonProperty public abstract String id(); @JsonProperty public abstract String title(); @JsonProperty public abstract String app_url(); ... } 3. Parse row subtypes @heylaurakelly

Slide 60

Slide 60 text

@AutoValue @JsonDeserialize(builder = AutoValue_LinkRowDataModel.Builder.class) @JsonTypeName("row:link") public abstract class LinkRowDataModel implements RowDataModel { @JsonProperty public abstract String id(); @JsonProperty public abstract String title(); @JsonProperty public abstract String app_url(); ... } 3. Parse row subtypes @heylaurakelly

Slide 61

Slide 61 text

JSON ReservationController ReservationFragment ArrayList rows( ) LinkRow MapRow TitleRow HostAvatarRow 1. JSON from server 2. Parse network response 3. Parse row subtypes 4. Configure UI models 5. Render components @heylaurakelly

Slide 62

Slide 62 text

Render with Epoxy https://github.com/airbnb/epoxy An open source library for building complex screens in a RecyclerView

Slide 63

Slide 63 text

class ReservationEpoxyController : TypedAirEpoxyController() { ... private fun RowDataModel.buildModel() { when (this) { is LinkRowDataModel -> buildModel() is MapRowDataModel -> buildModel() is TitleRowDataModel -> buildModel() is HostAvatarRowDataModel -> buildModel() } } } 4. Configure UI Models with Epoxy @heylaurakelly

Slide 64

Slide 64 text

class ReservationEpoxyController : TypedAirEpoxyController() { ... private fun LinkRowDataModel.buildModel() = basicRow { id([email protected]()) title(title()) onClickListener(navigationContoller.navigateToDeeplink(app_url())) } } 4. Configure UI Models with Epoxy @heylaurakelly

Slide 65

Slide 65 text

class ReservationEpoxyController : TypedAirEpoxyController() { ... private fun LinkRowDataModel.buildModel() = basicRow { id([email protected]()) title(title()) onClickListener(navigationContoller.navigateToDeeplink(app_url())) } } 4. Configure UI Models with Epoxy @heylaurakelly

Slide 66

Slide 66 text

class ReservationEpoxyController : TypedAirEpoxyController() { ... private fun LinkRowDataModel.buildModel() = basicRow { id([email protected]()) title(title()) onClickListener(navigationContoller.navigateToDeeplink(app_url())) } } 4. Configure UI Models with Epoxy @heylaurakelly

Slide 67

Slide 67 text

class ReservationEpoxyController : TypedAirEpoxyController() { ... private fun LinkRowDataModel.buildModel() = basicRow { id([email protected]()) title(title()) onClickListener(navigationContoller.navigateToDeeplink(app_url())) } } 4. Configure UI Models with Epoxy @heylaurakelly

Slide 68

Slide 68 text

class ReservationEpoxyController : TypedAirEpoxyController() { ... private fun LinkRowDataModel.buildModel() = basicRow { id([email protected]()) title(title()) onClickListener(navigationContoller.navigateToDeeplink(app_url())) } } 4. Configure UI Models with Epoxy @heylaurakelly

Slide 69

Slide 69 text

JSON ReservationController ReservationFragment ArrayList rows( ) LinkRow MapRow TitleRow HostAvatarRow 1. JSON from server 2. Parse network response 3. Parse row subtypes 4. Configure UI models 5. Render components @heylaurakelly

Slide 70

Slide 70 text

{
 "type": "place",
 "id": "123",
 "rows": [ {
 "type": "row:carousel",
 "image_urls": [ "sightglassImage.png” ]
 },
 {
 "type": “row:title“,
 "title": "Sightglass Coffee",
 "starts_at": “Sun, Sept 24, 7:00 PM"
 },
 {
 "type": "row:action",
 "actions": [ /* ... actions here */ ]
 },
 {
 "type": "row:map",
 "address": "1234 Main Street"
 }
 ]
 } @heylaurakelly

Slide 71

Slide 71 text

SERVER-DRIVEN SYSTEMS ACROSS AIRBNB @heylaurakelly

Slide 72

Slide 72 text

@heylaurakelly

Slide 73

Slide 73 text

BUILDING FORMS IS SIMPLE, RIGHT? @heylaurakelly

Slide 74

Slide 74 text

What’s so hard about building a user flow? @heylaurakelly

Slide 75

Slide 75 text

What’s so hard about building a user flow? @heylaurakelly

Slide 76

Slide 76 text

What’s so hard about building a user flow? @heylaurakelly

Slide 77

Slide 77 text

What’s so hard about building a user flow? @heylaurakelly

Slide 78

Slide 78 text

Branching Logic @heylaurakelly

Slide 79

Slide 79 text

Experimentation Control Treatment @heylaurakelly

Slide 80

Slide 80 text

Experimentation Control Treatment @heylaurakelly

Slide 81

Slide 81 text

Client-side logic @heylaurakelly

Slide 82

Slide 82 text

Wall-E JSON Payload Components Ordered list of screens Includes conditions for validation Questions Includes conditions for validation Answers Can be pre- populated, previously answered, or empty @heylaurakelly

Slide 83

Slide 83 text

API Response @heylaurakelly

Slide 84

Slide 84 text

API Response "questions": [ { "id": "sample_bool_question", "type": "BOOL" } ] @heylaurakelly

Slide 85

Slide 85 text

API Response "components": [ { "id": "doc_marquee", "phraseIdPrimary": "doc_marquee_title", "type": "DOCUMENT_MARQUEE" }, { "id": "sample_switch", "phraseIdPrimary": "switch_title", "questionId": "sample_bool_question", "type": "SWITCH_ROW" } ],

Slide 86

Slide 86 text

API Response "steps": [ { "componentIds": [ "doc_marquee", "sample_switch" ], "id": "initial_step", "nextButton": { "disabled": { "questionId": "sample_bool_question", "type": "ANSWER_EQUALS", "value": "false" } } } ], @heylaurakelly

Slide 87

Slide 87 text

API Response "settings": { "afterSubmitted": { "redirectUrl": "https://www.airbnb.com/superhost", "primaryButton": { "webHref": "https://www.airbnb.com/superhost" }, } } @heylaurakelly

Slide 88

Slide 88 text

NAVIGATING BETWEEN SCREENS @heylaurakelly

Slide 89

Slide 89 text

@heylaurakelly Hide and show steps based on state

Slide 90

Slide 90 text

@heylaurakelly Hide and show steps based on state

Slide 91

Slide 91 text

@heylaurakelly Hide and show steps based on state

Slide 92

Slide 92 text

LONA DYNAMIC UI @heylaurakelly

Slide 93

Slide 93 text

Scaling server-driven UI to hundreds of developers, sustainably. @heylaurakelly

Slide 94

Slide 94 text

Lona Dynamic UI @heylaurakelly

Slide 95

Slide 95 text

Lona Dynamic UI A unified format for views @heylaurakelly

Slide 96

Slide 96 text

Lona Dynamic UI A unified format for views Backend tooling to enforce the format @heylaurakelly

Slide 97

Slide 97 text

Lona Dynamic UI A unified format for views Backend tooling to enforce the format Client frameworks to render views @heylaurakelly

Slide 98

Slide 98 text

A UNIFIED FORMAT FOR DESCRIBING VIEWS @heylaurakelly

Slide 99

Slide 99 text

Design Language System @heylaurakelly

Slide 100

Slide 100 text

A sample Lona response { "id": "1", "type": "BasicRow", "params": { "content": { "title": "Paris", "subtitle": "Tap to search in Paris" }, "actions": { "onPress": { "case": "deepLink", "data": "airbnb://d/search?query=Paris" } } } } @heylaurakelly

Slide 101

Slide 101 text

BACKEND TOOLING TO ENFORCE THE FORMAT @heylaurakelly

Slide 102

Slide 102 text

• Kotlin domain-specific language (DSL) • The spec being in Kotlin makes it very easy to build tooling The Lona spec @heylaurakelly

Slide 103

Slide 103 text

The Lona spec Backend Service Lona Validation @heylaurakelly

Slide 104

Slide 104 text

CLIENT FRAMEWORKS TO RENDER VIEWS @heylaurakelly

Slide 105

Slide 105 text

// Can also be of type JSONObject val json: String = ... Lona on Android @heylaurakelly

Slide 106

Slide 106 text

// Can also be of type JSONObject val json: String = ... // This is where the magic happens val models: List> = LonaFile.make(json).makeModels() Lona on Android @heylaurakelly

Slide 107

Slide 107 text

// Can also be of type JSONObject val json: String = ... // This is where the magic happens val models: List> = LonaFile.make(json).makeModels() // Ready to be used in a controller, for example val controller = SimpleEpoxyController().apply { setModels(models) } Lona on Android @heylaurakelly

Slide 108

Slide 108 text

@JsonSubTypes({ @JsonSubTypes.Type( value = CarouselDataModel.class, name = “row:carousel” ), @JsonSubTypes.Type( value = TitleRowDataModel.class, name = “row:title” ), @JsonSubTypes.Type( value = ActionRowDataModel.class, name = “row:action" ) . . .
 }) Map to Data Models {
 "type": "place",
 "id": "123",
 "rows": [ {
 "type": "row:carousel",
 "image_urls":[ "sightglassImage.png” ]
 },
 {
 "type": “row:title“,
 "title": "Sightglass Coffee",
 "starts_at": “Sun, Sept 24, 7:00 PM"
 },
 {
 "type": "row:action",
 "actions": [ /* ... actions here */ ]
 } ...
 ]
 } API Response UI Rendering Rendering Framework Carousel Title Row Action Row @heylaurakelly

Slide 109

Slide 109 text

@JsonSubTypes({ @JsonSubTypes.Type( value = CarouselDataModel.class, name = “row:carousel” ), @JsonSubTypes.Type( value = TitleRowDataModel.class, name = “row:title” ), @JsonSubTypes.Type( value = ActionRowDataModel.class, name = “row:action" ) . . .
 }) Map to Views + Render {
 "type": "place",
 "id": "123",
 "rows": [ {
 "type": "row:carousel",
 "image_urls":[ "sightglassImage.png” ]
 },
 {
 "type": “row:title“,
 "title": "Sightglass Coffee",
 "starts_at": “Sun, Sept 24, 7:00 PM"
 },
 {
 "type": "row:action",
 "actions": [ /* ... actions here */ ]
 } ...
 ]
 } API Response Carousel Title Row Action Row @heylaurakelly

Slide 110

Slide 110 text

CODEGEN @heylaurakelly

Slide 111

Slide 111 text

Custom deserializer for building Epoxy models @heylaurakelly

Slide 112

Slide 112 text

public class BasicRowModel_ ... { public static BasicRowModel_ make(JSONObject json) { try { JSONObject params = json.getJSONObject(“params”); JSONObject content = params.getJSONObject(“content"); BasicRowModel_ model = new BasicRowModel_() // There should always be an id .id(json.getString("id")) .title(content.getString("title")) .onClickListener(clickListener); // Optional attributes if (content.has("subtitle")) { model.subtitleText(content.optString("subtitle")); } Custom deserializer for building Epoxy models @heylaurakelly

Slide 113

Slide 113 text

Custom deserializer for building Epoxy models public class BasicRowModel_ ... { public static BasicRowModel_ make(JSONObject json) { try { JSONObject params = json.getJSONObject(“params”); JSONObject content = params.getJSONObject(“content"); BasicRowModel_ model = new BasicRowModel_() // There should always be an id .id(json.getString("id")) .title(content.getString("title")); // Optional attributes if (content.has("subtitle")) { model.subtitleText(content.optString("subtitle")); } @heylaurakelly

Slide 114

Slide 114 text

• Lona has an extensive set of test JSON responses • They’re generated directly from the Lona spec • The test JSON responses are stubbed into client CI tests • Changes to clients and the spec are rigorously tested automatically in CI Codegen for continuous integration testing @heylaurakelly

Slide 115

Slide 115 text

DIFFERENT LEVELS OF INTEGRATION @heylaurakelly

Slide 116

Slide 116 text

Lona Dynamic UI A unified format for views Backend tooling to enforce the format Client frameworks to render views @heylaurakelly

Slide 117

Slide 117 text

Lona Dynamic UI Wall E backend service Wall E client A unified format for views @heylaurakelly

Slide 118

Slide 118 text

• A single component in a screen rendered by a non-Lona system • A full screen rendered by Lona • A set of screens rendered by Lona Levels of integration on the client @heylaurakelly

Slide 119

Slide 119 text

VERSIONING @heylaurakelly

Slide 120

Slide 120 text

1. Build the initial system and components 2. Add new components and properties 3. Push the changes to a new app version 4. Turn on the API changes Versioning Some users don’t update @heylaurakelly

Slide 121

Slide 121 text

A server-driven system has to accommodate an API that evolves faster than some users upgrade @heylaurakelly

Slide 122

Slide 122 text

• Versioning by an explicit client version number • Lona is backwards compatible • Have fallbacks for older clients API evolution @heylaurakelly

Slide 123

Slide 123 text

HOW LONA SCALES @heylaurakelly

Slide 124

Slide 124 text

• Let teams decide what level of integration makes sense • Make full use of our Design Language System • Validate on the backend • Eliminate boilerplate data models • Codegen Epoxy model classes and CI tests • Explictly version and intentional fallbacks Scaling server-driven UI with Lona @heylaurakelly

Slide 125

Slide 125 text

@heylaurakelly

Slide 126

Slide 126 text

Thanks @heylaurakelly Reservations contributors Laura Kelly, Eric Horacek, Tyler Hedrick, Jenn Tilton, Callie Callaway, Laura Xu, Andy Bartholomew Wall-E contributors Truman Cranor, Kevin Almanza, Garrett Berg, Josh Freeman, Steven Liu, Chris Talley, Noah Hendrix Lona Dynamic UI contributors Laura Kelly, Nathanael Silverman, Kieraj Mumick, Tae Kim