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.

1f0849b04deaf7e64171803e3bcf5f1f?s=128

Laura Kelly

June 26, 2018
Tweet

Transcript

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

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

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

  5. Airbnb is adding new products all the time @heylaurakelly Homes

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

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

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

    the same screen, multiplying our efforts across the codebase
  9. @heylaurakelly

  10. …and across platforms @heylaurakelly

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

  12. What would an ideal system be? @heylaurakelly

  13. Easy to understand @heylaurakelly

  14. Flexible to adapt to designers @heylaurakelly

  15. Launch without a Play Store release @heylaurakelly

  16. Minimize repetition @heylaurakelly

  17. Easy to maintain @heylaurakelly

  18. Server-driven UI @heylaurakelly

  19. 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
  20. @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
  21. @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
  22. @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 */ ]
 } ...
 ]
 }
  23. @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
  24. @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
  25. @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
  26. @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
  27. @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
  28. @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
  29. @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
  30. @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
  31. @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
  32. Benefits we saw after building the system @heylaurakelly

  33. Build once @heylaurakelly

  34. Easily accommodates changing minds @heylaurakelly

  35. Reuse existing UI component libraries @heylaurakelly

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

  37. Android Deep Dive @heylaurakelly

  38. 1. JSON from server JSON @heylaurakelly

  39. 1. JSON from server JSON 2. Parse network response ArrayList<Row>

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

    Parse row subtypes ArrayList<Row> rows( ) LinkRow MapRow TitleRow HostAvatarRow @heylaurakelly
  41. 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
  42. 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
  43. 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
  44. @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
  45. @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
  46. 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
  47. @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
  48. @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
  49. @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
  50. @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
  51. @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
  52. @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
  53. @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
  54. 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
  55. Render with Epoxy HTTPS://GITHUB.COM/AIRBNB/EPOXY Open source library for building complex

    screens in a RecyclerView
  56. @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
  57. @heylaurakelly class ReservationEpoxyController : TypedAirEpoxyController<Reservation>() { ... private fun LinkRowDataModel.buildModel()

    = basicRow { id(this@buildModel.id()) title(title()) onClickListener(navigationContoller.navigateToDeeplink(app_url())) } } 4) Configure UI Models with Epoxy
  58. 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
  59. {
 "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
  60. How we’re using server-driven systems across Airbnb @heylaurakelly

  61. @heylaurakelly

  62. Building forms is simple, right? @heylaurakelly

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

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

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

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

  67. @heylaurakelly Client-side logic

  68. 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
  69. Hide and show steps based on state @heylaurakelly

  70. Lona Dynamic UI @heylaurakelly

  71. • 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
  72. Think you want to try it? @heylaurakelly

  73. • 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
  74. @heylaurakelly

  75. 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