Slide 1

Slide 1 text

Rapidly iterating across platforms using server-driven UI LAURA KELLY @heylaurakelly

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

1. The problem we were trying to solve 2. Why server-driven UI fit the bill 3. Deep-dive into Android implementation 4. Case studies from Airbnb @heylaurakelly Outline

Slide 4

Slide 4 text

@heylaurakelly Airbnb is adding new products all the time Homes

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

@heylaurakelly

Slide 10

Slide 10 text

…and across platforms @heylaurakelly

Slide 11

Slide 11 text

There’s got to be a better way. @heylaurakelly

Slide 12

Slide 12 text

What would an ideal system be? @heylaurakelly

Slide 13

Slide 13 text

Easy to understand @heylaurakelly

Slide 14

Slide 14 text

Flexible to adapt to designers @heylaurakelly

Slide 15

Slide 15 text

Launch without a Play Store release @heylaurakelly

Slide 16

Slide 16 text

Minimize repetition @heylaurakelly

Slide 17

Slide 17 text

Easy to maintain @heylaurakelly

Slide 18

Slide 18 text

Server-driven UI @heylaurakelly

Slide 19

Slide 19 text

What is server-driven UI? {
 "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"
 }
 ]
 } Example API response @heylaurakelly

Slide 20

Slide 20 text

@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"
 }
 ]
 } API Response

Slide 21

Slide 21 text

@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"
 }
 ]
 } API Response

Slide 22

Slide 22 text

@heylaurakelly API Response {
 "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 */ ]
 } ...
 ]
 }

Slide 23

Slide 23 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" ) . . .
 }) {
 "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 */ ]
 } ...
 ]
 } @heylaurakelly Map to Data Models API Response

Slide 24

Slide 24 text

@heylaurakelly export default { 'row:carousel': CarouselRow, 'row:title': TitleRow, 'row:action': ActionRow, . . .
 } Frontend Web Code {
 "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

Slide 25

Slide 25 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 26

Slide 26 text

@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 {
 "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

Slide 27

Slide 27 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 28

Slide 28 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 29

Slide 29 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 Carousel Title Row Action Row Rendering Framework @heylaurakelly

Slide 30

Slide 30 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 Title Row Action Row Rendering Framework Map Row @heylaurakelly

Slide 31

Slide 31 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 Action Row Title Row Rendering Framework Map Row @heylaurakelly

Slide 32

Slide 32 text

Benefits we saw after building the system @heylaurakelly

Slide 33

Slide 33 text

Build once @heylaurakelly

Slide 34

Slide 34 text

Easily accommodates changing minds @heylaurakelly

Slide 35

Slide 35 text

Reuse existing UI component libraries @heylaurakelly

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

Android Deep Dive @heylaurakelly

Slide 38

Slide 38 text

1. JSON from server JSON @heylaurakelly

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

JSON 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 ReservationController ReservationFragment @heylaurakelly

Slide 47

Slide 47 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 48

Slide 48 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 49

Slide 49 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 50

Slide 50 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 51

Slide 51 text

@heylaurakelly @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(); @JsonProperty public abstract String loggingId(); ... } 3. Parse row subtypes

Slide 52

Slide 52 text

@heylaurakelly @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(); @JsonProperty public abstract String loggingId(); ... } 3. Parse row subtypes

Slide 53

Slide 53 text

@heylaurakelly @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(); @JsonProperty public abstract String loggingId(); ... } 3. Parse row subtypes

Slide 54

Slide 54 text

JSON 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 ReservationController ReservationFragment @heylaurakelly

Slide 55

Slide 55 text

Render with Epoxy HTTPS://GITHUB.COM/AIRBNB/EPOXY Open source library for building complex screens in a RecyclerView

Slide 56

Slide 56 text

@heylaurakelly 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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

JSON 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 ReservationController ReservationFragment @heylaurakelly

Slide 59

Slide 59 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 60

Slide 60 text

How we’re using server-driven systems across Airbnb @heylaurakelly

Slide 61

Slide 61 text

@heylaurakelly

Slide 62

Slide 62 text

Building forms is simple, right? @heylaurakelly

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

@heylaurakelly Client-side logic

Slide 68

Slide 68 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 69

Slide 69 text

Hide and show steps based on state @heylaurakelly

Slide 70

Slide 70 text

Lona Dynamic UI @heylaurakelly

Slide 71

Slide 71 text

• A unified format for describing views • Can be used standalone to natively parse and render pages • Can also underlie existing server-driven systems Lona Dynamic UI @heylaurakelly

Slide 72

Slide 72 text

Think you want to try it? @heylaurakelly

Slide 73

Slide 73 text

• API design is key • Think about platform differences early on • Know how server-driven you want to go Tips for building a server-driven system @heylaurakelly

Slide 74

Slide 74 text

@heylaurakelly

Slide 75

Slide 75 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 Nathanael Silverman, Kieraj Mumick, Devin Abott, Gabe G'Sell