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

Rapidly iterating across platforms using server-driven UI

Rapidly iterating across platforms using server-driven UI

Often companies with large mobile user bases find that they have to balance rapid iteration with the amount of work involved in launching and coordinating a product on multiple platforms (web, native Android, and native iOS).

Faced with exactly this question for the Airbnb reservation system, we designed a server-driven UI framework for web, iOS, and Android that allows us to launch new types of reservations on all platforms with a simple backend change. It uses a commonly shared API that is parsed on the client and rendered into a set of corresponding components supported on all platforms.

The talk also covers the ways other teams at Airbnb are using server-driven UI to do cross-platform product development.

Laura Kelly

June 26, 2018
Tweet

More Decks by Laura Kelly

Other Decks in Programming

Transcript

  1. • Android Engineer @ Airbnb • Previously front-end web •

    Trip platform team • Powerlifter • Whiskey connoisseur @heylaurakelly Who I am
  2. 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
  3. @heylaurakelly Airbnb is adding new products all the time Homes

    Experiences Restaurants Coworking Spaces
  4. @heylaurakelly Homes Experiences Restaurants Coworking Spaces We were rebuilding nearly

    the same screen, multiplying our efforts across the codebase
  5. 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
  6. @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
  7. @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
  8. @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 */ ]
 } ...
 ]
 }
  9. @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
  10. @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
  11. @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
  12. @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
  13. @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
  14. @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
  15. @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
  16. @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
  17. @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
  18. 1. JSON from server JSON 2. Parse network response 3.

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

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

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

    Parse row subtypes 4. Configure UI models ArrayList<Row> rows( ) LinkRow MapRow TitleRow HostAvatarRow 5. Render components ReservationController ReservationFragment @heylaurakelly
  22. @AutoValue @JsonDeserialize(builder = AutoValue_Reservation.Builder.class) public abstract class Reservation { @JsonProperty

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

    public abstract String primary_key(); @JsonProperty public abstract ArrayList<RowDataModel> rows(); ... } @heylaurakelly 2. Parse network response
  24. JSON ArrayList<Row> 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
  25. @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
  26. @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
  27. @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
  28. @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
  29. @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
  30. @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
  31. @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
  32. JSON ArrayList<Row> 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
  33. @heylaurakelly class ReservationEpoxyController : TypedAirEpoxyController<Reservation>() { ... private fun RowDataModel.buildModel()

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

    = basicRow { id([email protected]()) title(title()) onClickListener(navigationContoller.navigateToDeeplink(app_url())) } } 4) Configure UI Models with Epoxy
  35. JSON ArrayList<Row> 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
  36. {
 "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
  37. 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
  38. • 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
  39. • 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
  40. 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