Ce diaporama a bien été signalé.
Nous utilisons votre profil LinkedIn et vos données d’activité pour vous proposer des publicités personnalisées et pertinentes. Vous pouvez changer vos préférences de publicités à tout moment.

Rapidly Iterating Across Platforms with Server-Driven UI

298 vues

Publié le

Can you have a full-featured UI on web and native and iterate quickly at the same time? In this talk, learn how we designed a server-driven UI framework for web, iOS, and Android that allows us to launch new features across web and native with a simple backend change.

Publié dans : Ingénierie
  • Soyez le premier à commenter

Rapidly Iterating Across Platforms with Server-Driven UI

  1. 1. Rapidlyiteratingacross platformsusing server-drivenUI LAURA KELLY
  2. 2. • Android Engineer • Previously front-end web • Trips team • Itinerary, reservations, and trip planning • Powerlifter • Whiskey connoisseur WhoIam
  3. 3. Airbnbisaddingnewproductsallthetime Homes
  4. 4. Airbnbisaddingnewproductsallthetime Homes Experiences
  5. 5. Airbnbisaddingnewproductsallthetime Homes Experiences Restaurants
  6. 6. Airbnbisaddingnewproductsallthetime Homes Experiences Restaurants Coworking Spaces
  7. 7. Homes Experiences Restaurants Coworking Spaces Wewererebuildingnearlythe samescreen, multiplyingoureffortsacross thecodebase
  8. 8. …andacrossplatforms
  9. 9. There’sgottobeabetterway.
  10. 10. Whatwouldanidealsystembe?
  11. 11. Easytounderstand
  12. 12. Flexibletoadapttodesigners
  13. 13. Abletolaunchwithoutanew PlayStore/AppStorerelease
  14. 14. Minimizerepetition
  15. 15. Easytomaintain
  16. 16. ?
  17. 17. Server-drivenUI
  18. 18. Whatisserver-drivenUI? {
  "type": "place",
  "id": "123",
  "marquee": {
    "type": "marquee:image",
    "image_urls": [ "sightglassImage.png” ]
  },
  "rows": [
    {
      "type": “row:header_title_subtitle“,
      "title": "Sightglass Coffee",
      "starts_at": “Sun, Sept 24, 7:00 PM"
    },
    {
      "type": "row:action",
      "actions": [ /* ... actions here */ ]
    },
    {
      "type": "row:poi_map",
      "address": "1234 Main Street"
    }
  ]
 } ExampleAPIresponse
  19. 19. {
  "type": "place",
  "id": "123",
  "marquee": {
    "type": "marquee:image",
    "image_urls": [ "sightglassImage.png” ]
  },
  "rows": [
    {
      "type": “row:header_subtitle_title",
      "title": "Sightglass Coffee",
      "starts_at": “Sun, Sept 24, 7:00 PM"
    },
    {
      "type": "row:action",
      "actions": [ /* ... actions here */ ]
    },
    {
      "type": "row:poi_map",
      "address": "1234 Main Street"
    }
  ]
 } APIResponse
  20. 20. {
  "type": "place",
  "id": "123",
  "marquee": {
    "type": "marquee:image",
    "image_urls": [ "sightglassImage.png” ]
  },
  "rows": [
    {
      "type": “row:header_subtitle_title",
      "title": "Sightglass Coffee",
      "starts_at": “Sun, Sept 24, 7:00 PM"
    },
    {
      "type": "row:action",
      "actions": [ /* ... actions here */ ]
    },
    {
      "type": "row:poi_map",
      "address": "1234 Main Street"
    }
  ]
 } APIResponse
  21. 21. {
  "type": "place",
  "id": "123",
  "marquee": {
    "type": "marquee:image",
    "image_urls": [ "sightglassImage.png” ]
  },
  "rows": [
    {
      "type": “row:header_subtitle_title",
      "title": "Sightglass Coffee",
      "starts_at": “Sun, Sept 24, 7:00 PM"
    },
    {
      "type": "row:action",
      "actions": [ /* ... actions here */ ]
    },
    {
      "type": "row:poi_map",
      "address": "1234 Main Street"
    }
  ]
 } APIResponse
  22. 22. {
  "type": "place",
  "id": "123",
  "marquee": {
    "type": "marquee:image",
    "image_urls": [ "sightglassImage.png” ]
  },
  "rows": [
    {
      "type": “row:header_subtitle_title",
      "title": "Sightglass Coffee",
      "starts_at": “Sun, Sept 24, 7:00 PM"
    },
    {
      "type": "row:action",
      "actions": [ /* ... actions here */ ]
    },
    {
      "type": "row:poi_map",
      "address": "1234 Main Street"
    }
  ]
 } @JsonSubTypes({ @JsonSubTypes.Type( value = HeaderSubtitleTitleRowDataModel.class, name = “row:header_subtitle_title" ), @JsonSubTypes.Type( value = ActionRowDataModel.class, name = “row:action" ), @JsonSubTypes.Type( value = POIMapRowDataModel.class, name = “row:poi_map” ), . . .
 }) APIResponse AndroidCode
  23. 23. @JsonSubTypes({ @JsonSubTypes.Type( value = HeaderSubtitleTitleRowDataModel.class, name = “row:header_subtitle_title" ), @JsonSubTypes.Type( value = ActionRowDataModel.class, name = “row:action" ), @JsonSubTypes.Type( value = POIMapRowDataModel.class, name = “row:poi_map” ), . . .
 }) {
  "type": "place",
  "id": "123",
  "marquee": {
    "type": "marquee:image",
    "image_urls": [ "sightglassImage.png” ]
  },
  "rows": [
    {
      "type": “row:header_subtitle_title",
      "title": "Sightglass Coffee",
      "starts_at": “Sun, Sept 24, 7:00 PM"
    },
    {
      "type": "row:action",
      "actions": [ /* ... actions here */ ]
    },
    {
      "type": "row:poi_map",
      "address": "1234 Main Street"
    }
  ]
 } APIResponse AndroidCode
  24. 24. @JsonSubTypes({ @JsonSubTypes.Type( value = HeaderSubtitleTitleRowDataModel.class, name = “row:header_subtitle_title" ), @JsonSubTypes.Type( value = ActionRowDataModel.class, name = “row:action" ), @JsonSubTypes.Type( value = POIMapRowDataModel.class, name = “row:poi_map” ), . . .
 }) {
  "type": "place",
  "id": "123",
  "marquee": {
    "type": "marquee:image",
    "image_urls": [ "sightglassImage.png” ]
  },
  "rows": [
    {
      "type": “row:header_subtitle_title",
      "title": "Sightglass Coffee",
      "starts_at": “Sun, Sept 24, 7:00 PM"
    },
    {
      "type": "row:action",
      "actions": [ /* ... actions here */ ]
    },
    {
      "type": "row:poi_map",
      "address": "1234 Main Street"
    }
  ]
 } APIResponse AndroidCode
  25. 25. @JsonSubTypes({ @JsonSubTypes.Type( value = HeaderSubtitleTitleRowDataModel.class, name = “row:header_subtitle_title" ), @JsonSubTypes.Type( value = ActionRowDataModel.class, name = “row:action" ), @JsonSubTypes.Type( value = POIMapRowDataModel.class, name = “row:poi_map” ), . . .
 }) {
  "type": "place",
  "id": "123",
  "marquee": {
    "type": "marquee:image",
    "image_urls": [ "sightglassImage.png” ]
  },
  "rows": [
    {
      "type": “row:header_subtitle_title",
      "title": "Sightglass Coffee",
      "starts_at": “Sun, Sept 24, 7:00 PM"
    },
    {
      "type": "row:action",
      "actions": [ /* ... actions here */ ]
    },
    {
      "type": "row:poi_map",
      "address": "1234 Main Street"
    }
  ]
 } APIResponse AndroidCode
  26. 26. {
  "type": "place",
  "id": "123",
  "marquee": {
    "type": "marquee:image",
    "image_urls": [ "sightglassImage.png” ]
  },
  "rows": [
    {
      "type": “row:header_subtitle_title",
      "title": "Sightglass Coffee",
      "starts_at": “Sun, Sept 24, 7:00 PM"
    },
    {
      "type": "row:action",
      "actions": [ /* ... actions here */ ]
    },
    {
      "type": "row:poi_map",
      "address": "1234 Main Street"
    }
  ]
 } export default { 'row:header_subtitle_title': HeaderSubtitleTitleRow, 'row:action': ActionRow, 'row:poi_map': MapRow, . . .
 } ReactWebCode APIResponse
  27. 27. {
  "type": "place",
  "id": "123",
  "marquee": {
    "type": "marquee:image",
    "image_urls": [ "sightglassImage.png” ]
  },
  "rows": [
    {
      "type": “row:header_subtitle_title",
      "title": "Sightglass Coffee",
      "starts_at": “Sun, Sept 24, 7:00 PM"
    },
    {
      "type": "row:action",
      "actions": [ /* ... actions here */ ]
    },
    {
      "type": "row:poi_map",
      "address": "1234 Main Street"
    }
  ]
 } export default { 'row:header_subtitle_title': HeaderSubtitleTitleRow, 'row:action': ActionRow, 'row:poi_map': MapRow, . . .
 } APIResponse ReactWebCode
  28. 28. {
  "type": "place",
  "id": "123",
  "marquee": {
    "type": "marquee:image",
    "image_urls": [ "sightglassImage.png” ]
  },
  "rows": [
    {
      "type": “row:header_subtitle_title",
      "title": "Sightglass Coffee",
      "starts_at": “Sun, Sept 24, 7:00 PM"
    },
    {
      "type": "row:action",
      "actions": [ /* ... actions here */ ]
    },
    {
      "type": "row:poi_map",
      "address": "1234 Main Street"
    }
  ]
 } @JsonSubTypes({ @JsonSubTypes.Type( value = HeaderSubtitleTitleRowDataModel.class, name = “row:header_subtitle_title" ), @JsonSubTypes.Type( value = ActionRowDataModel.class, name= “row:action" ), @JsonSubTypes.Type( value = POIMapRowDataModel.class, name = “row:poi_map” ), . . .
 }) APIResponse AndroidCode UIRendering
  29. 29. @JsonSubTypes({ @JsonSubTypes.Type( value = HeaderSubtitleTitleRowDataModel.class, name = “row:header_subtitle_title" ), @JsonSubTypes.Type( value = ActionRowDataModel.class, name= “row:action" ), @JsonSubTypes.Type( value = POIMapRowDataModel.class, name = “row:poi_map” ), . . .
 }) {
  "type": "place",
  "id": "123",
  "marquee": {
    "type": "marquee:image",
    "image_urls": [ "sightglassImage.png” ]
  },
  "rows": [
    {
      "type": “row:header_subtitle_title",
      "title": "Sightglass Coffee",
      "starts_at": “Sun, Sept 24, 7:00 PM"
    },
    {
      "type": "row:action",
      "actions": [ /* ... actions here */ ]
    },
    {
      "type": "row:poi_map",
      "address": "1234 Main Street"
    }
  ]
 } APIResponse AndroidCode UIRendering
  30. 30. @JsonSubTypes({ @JsonSubTypes.Type( value = HeaderSubtitleTitleRowDataModel.class, name = “row:header_subtitle_title" ), @JsonSubTypes.Type( value = ActionRowDataModel.class, name= “row:action" ), @JsonSubTypes.Type( value = POIMapRowDataModel.class, name = “row:poi_map” ), . . .
 }) {
  "type": "place",
  "id": "123",
  "marquee": {
    "type": "marquee:image",
    "image_urls": [ "sightglassImage.png” ]
  },
  "rows": [
    {
      "type": “row:header_subtitle_title",
      "title": "Sightglass Coffee",
      "starts_at": “Sun, Sept 24, 7:00 PM"
    },
    {
      "type": "row:action",
      "actions": [ /* ... actions here */ ]
    },
    {
      "type": "row:poi_map",
      "address": "1234 Main Street"
    }
  ]
 } APIResponse AndroidCode UIRendering
  31. 31. @JsonSubTypes({ @JsonSubTypes.Type( value = HeaderSubtitleTitleRowDataModel.class, name = “row:header_subtitle_title" ), @JsonSubTypes.Type( value = ActionRowDataModel.class, name= “row:action" ), @JsonSubTypes.Type( value = POIMapRowDataModel.class, name = “row:poi_map” ), . . .
 }) {
  "type": "place",
  "id": "123",
  "marquee": {
    "type": "marquee:image",
    "image_urls": [ "sightglassImage.png” ]
  },
  "rows": [
    {
      "type": “row:header_subtitle_title",
      "title": "Sightglass Coffee",
      "starts_at": “Sun, Sept 24, 7:00 PM"
    },
    {
      "type": "row:action",
      "actions": [ /* ... actions here */ ]
    },
    {
      "type": "row:poi_map",
      "address": "1234 Main Street"
    }
  ]
 } APIResponse AndroidCode UIRendering
  32. 32. {
  "type": "place",
  "id": "123",
  "marquee": {
    "type": "marquee:image",
    "image_urls": [ "sightglassImage.png” ]
  },
  "rows": [
    {
      "type": “row:header_subtitle_title",
      "title": "Sightglass Coffee",
      "starts_at": “Sun, Sept 24, 7:00 PM"
    },
    {
      "type": "row:action",
      "actions": [ /* ... actions here */ ]
    },
    {
      "type": "row:poi_map",
      "address": "1234 Main Street"
    }
  ]
 } API RESPONSE Howitworks @JsonSubTypes({ @JsonSubTypes.Type( value = HeaderSubtitleTitleRowDataModel.class, name = “row:header_subtitle_title" ), @JsonSubTypes.Type( value = ActionRowDataModel.class, name=“row:action" ), @JsonSubTypes.Type( value = POIMapRowDataModel.class, name = “row:poi_map” ), . . .
 } UI RENDERING FRAMEWORK (React or Epoxy)
  33. 33. Howitworks {
  "type": "place",
  "id": "123",
  "marquee": {
    "type": "marquee:image",
    "image_urls": [ "sightglassImage.png” ]
  },
  "rows": [
    {
      "type": “row:header_subtitle_title",
      "title": "Sightglass Coffee",
      "starts_at": “Sun, Sept 24, 7:00 PM"
    },
    {
      "type": "row:action",
      "actions": [ /* ... actions here */ ]
    },
    {
      "type": "row:poi_map",
      "address": "1234 Main Street"
    }
  ]
 } API RESPONSE @JsonSubTypes({ @JsonSubTypes.Type( value = HeaderSubtitleTitleRowDataModel.class, name = “row:header_subtitle_title" ), @JsonSubTypes.Type( value = ActionRowDataModel.class, name=“row:action" ), @JsonSubTypes.Type( value = POIMapRowDataModel.class, name = “row:poi_map” ), . . .
 } UI RENDERING FRAMEWORK (React or Epoxy)
  34. 34. Bonusesthatcamewiththissystem
  35. 35. Buildonce
  36. 36. Easilyaccommodateschangingminds
  37. 37. ReuseexistingUIcomponentlibraries
  38. 38. Iterate,reconfigure,andexperiment onourowntime
  39. 39. • Think about your use case • Think about platform differences early on • Know your limits Tipsforbuildingouta server-drivenUIsystem
  40. 40. • Mobile-forward companies keen on visual consistency • Repetition, and lots of it! • Limited client-side engineers • You want to launch on all platforms at the same time • Teams that communicate well Whoshould Whoshouldn’t
  41. 41. Whoshould • No native app • Product looks different across platforms and devices • Unique screens, minimal visual overlap Whoshouldn’t • Mobile-forward companies keen on visual consistency • Repetition, and lots of it! • Limited client-side engineers • You want to launch on all platforms at the same time • Teams that communicate well
  42. 42. THANKS LAURA KELLY

×