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

Philly ETE 2019 Rapidly Iterating Across Platfo...

Philly ETE 2019 Rapidly Iterating Across Platforms Using Server-Driven UI

Philly ETE 2019

Laura Kelly

April 24, 2019
Tweet

More Decks by Laura Kelly

Other Decks in Technology

Transcript

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

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

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

    the same screen, multiplying our efforts across the codebase
  5. 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"
 }
 ]
 }
  6. {
 "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
  7. LinkRow { id, type, deeplink } ActionRow { id, type,

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

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

    actions } CarouselRow { id, type, imageUrls} MapRow { id, type, lat, long } Row { id, type } @heylaurakelly
  10. {
 "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
  11. {
 "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
  12. {
 "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
  13. {
 "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
  14. {
 "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, . . .
 }
  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 @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 @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 @heylaurakelly
  18. @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
  19. @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
  20. @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
  21. @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
  22. JSON ArrayList<Row> rows( ) LinkRow MapRow TitleRow HostAvatarRow 1. JSON

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

    1. JSON from server 2. Parse network response 3. Parse row subtypes 4. Configure UI models @heylaurakelly
  24. JSON ReservationController ReservationFragment 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 @heylaurakelly
  25. JSON ReservationController ReservationFragment 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 @heylaurakelly
  26. @AutoValue @JsonDeserialize(builder = AutoValue_Reservation.Builder.class) public abstract class Reservation { @JsonProperty

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

    public abstract String primary_key(); @JsonProperty public abstract ArrayList<RowDataModel> rows(); ... } 2. Parse network response @heylaurakelly
  28. JSON ReservationController ReservationFragment 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 @heylaurakelly
  29. @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
  30. @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
  31. @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
  32. @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
  33. @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
  34. @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
  35. @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
  36. JSON ReservationController ReservationFragment 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 @heylaurakelly
  37. 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 @heylaurakelly
  38. 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 @heylaurakelly
  39. 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 @heylaurakelly
  40. 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 @heylaurakelly
  41. 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 @heylaurakelly
  42. 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 @heylaurakelly
  43. JSON ReservationController ReservationFragment 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 @heylaurakelly
  44. {
 "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
  45. 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
  46. 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" } ],
  47. API Response "steps": [ { "componentIds": [ "doc_marquee", "sample_switch" ],

    "id": "initial_step", "nextButton": { "disabled": { "questionId": "sample_bool_question", "type": "ANSWER_EQUALS", "value": "false" } } } ], @heylaurakelly
  48. Lona Dynamic UI A unified format for views Backend tooling

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

    to enforce the format Client frameworks to render views @heylaurakelly
  50. 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
  51. • Kotlin domain-specific language (DSL) • The spec being in

    Kotlin makes it very easy to build tooling The Lona spec @heylaurakelly
  52. // Can also be of type JSONObject val json: String

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

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

    = ... // This is where the magic happens val models: List<EpoxyModel<*>> = LonaFile.make(json).makeModels() // Ready to be used in a controller, for example val controller = SimpleEpoxyController().apply { setModels(models) } Lona on Android @heylaurakelly
  55. @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
  56. @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
  57. 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
  58. 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
  59. • 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
  60. Lona Dynamic UI A unified format for views Backend tooling

    to enforce the format Client frameworks to render views @heylaurakelly
  61. Lona Dynamic UI Wall E backend service Wall E client

    A unified format for views @heylaurakelly
  62. • 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
  63. 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
  64. A server-driven system has to accommodate an API that evolves

    faster than some users upgrade @heylaurakelly
  65. • Versioning by an explicit client version number • Lona

    is backwards compatible • Have fallbacks for older clients API evolution @heylaurakelly
  66. • 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
  67. 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