Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
Laura Kelly - Rapidly iterating across platform...
Search
droidcon Berlin
July 17, 2018
Programming
120
0
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
Laura Kelly - Rapidly iterating across platforms using server-driven UI
droidcon Berlin
July 17, 2018
More Decks by droidcon Berlin
See All by droidcon Berlin
Jon Markoff - Best practice for apps
droidcon_berlin_2018
0
230
Jon Markoff - Voice in the enterprise
droidcon_berlin_2018
0
83
Michael Jess - Enabling enterprise mobility with SAP
droidcon_berlin_2018
0
130
Ronen Sabag - Lean async code with Kotlin’s coroutines
droidcon_berlin_2018
0
86
Boris Farber & Nikita Kozlov - The_Build_Side_of_Android_App
droidcon_berlin_2018
0
220
Zan Markan - The state of Kotlin
droidcon_berlin_2018
0
87
Miquel Beltran - No More □ (tofu) Mastering Emoji on Android
droidcon_berlin_2018
0
140
Laurent Gasser & Jeremy Rochot - Sharing a success story - A low cost, Customer driven and co-developed Android EMM
droidcon_berlin_2018
0
330
Hoi Lam - Adding ML Kit to Android Things And some TensorFlow things
droidcon_berlin_2018
1
240
Other Decks in Programming
See All in Programming
地域 SRE コミュニティ最前線 - ホンマでっかSRE勉強会
tk3fftk
0
290
コーディングルールの鮮度を保ちたい for SRE NEXT 2026 / keep-fresh-go-internal-conventions-sre-next-2026
handlename
0
160
作るコストが小さくなった時代 幸せに働くために改めて考えたいこと 〜エンジニアとして価値を出し続けるために注視している二分野〜
yuppeeng
0
140
yield再入門 #phpcon
o0h
PRO
0
850
Google Apps Script で Ruby を動かす
kawahara
0
120
型も通る、synthも通る、それでも危ない 〜AIのCDKの権限とコストを機械で検証する〜 / It Passes Type Checks, It Passes Synth Checks, but It’s Still Risky — Automatically Verifying Permissions and Costs in AI’s CDK —
seike460
PRO
1
500
AI時代に設計が 最大の生産性レバーになる 意図駆動開発とデータを消さない設計|Don't Delete Your Data or Your Intent — Design as the Deepest Lever in the AI Era
tomohisa
0
120
ルールを書いて終わらせないハーネスエンジニアリング
yug1224
4
1.8k
GDG Korea Android: 2026 I/O Extended ~ What's new in Android development tools
pluu
0
200
JAWS-UG横浜 #102 AWSサ終供養LT会 成仏できない AWS サービスたち 〜本日、三体供養します〜
maroon1st
0
280
ビデオ通話が繋がる0.2秒で何が起きているのか
supurazako
2
160
複数の Claude Code が"放置"されてしまう問題をCLI ダッシュボードを自作して解決した話
sumihiro3
0
580
Featured
See All Featured
Agile Actions for Facilitating Distributed Teams - ADO2019
mkilby
0
230
End of SEO as We Know It (SMX Advanced Version)
ipullrank
3
4.3k
The Invisible Side of Design
smashingmag
301
52k
JAMstack: Web Apps at Ludicrous Speed - All Things Open 2022
reverentgeek
1
510
Why Our Code Smells
bkeepers
PRO
340
58k
Amusing Abliteration
ianozsvald
1
240
WENDY [Excerpt]
tessaabrams
11
39k
Are puppies a ranking factor?
jonoalderson
1
3.7k
Redefining SEO in the New Era of Traffic Generation
szymonslowik
1
370
Crafting Experiences
bethany
1
230
Automating Front-end Workflow
addyosmani
1370
210k
Bridging the Design Gap: How Collaborative Modelling removes blockers to flow between stakeholders and teams @FastFlow conf
baasie
0
620
Transcript
Rapidly iterating across platforms using server-driven UI LAURA KELLY @heylaurakelly
• Android Engineer @ Airbnb • Previously front-end web •
Trip platform team • Powerlifter • Whiskey connoisseur @heylaurakelly Who I am
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
@heylaurakelly Airbnb is adding new products all the time Homes
Airbnb is adding new products all the time @heylaurakelly Homes
Experiences
Airbnb is adding new products all the time @heylaurakelly Homes
Experiences Restaurants
@heylaurakelly Airbnb is adding new products all the time Homes
Experiences Restaurants Coworking Spaces
@heylaurakelly Homes Experiences Restaurants Coworking Spaces We were rebuilding nearly
the same screen, multiplying our efforts across the codebase
@heylaurakelly
…and across platforms @heylaurakelly
There’s got to be a better way. @heylaurakelly
What would an ideal system be? @heylaurakelly
Easy to understand @heylaurakelly
Flexible to adapt to designers @heylaurakelly
Launch without a Play Store release @heylaurakelly
Minimize repetition @heylaurakelly
Easy to maintain @heylaurakelly
Server-driven UI @heylaurakelly
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
@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
@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
@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 */ ] } ... ] }
@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
@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
@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
@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
@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
@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
@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
@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
@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
Benefits we saw after building the system @heylaurakelly
Build once @heylaurakelly
Easily accommodates changing minds @heylaurakelly
Reuse existing UI component libraries @heylaurakelly
Iterate, reconfigure, and experiment on our own time @heylaurakelly
Android Deep Dive @heylaurakelly
1. JSON from server JSON @heylaurakelly
1. JSON from server JSON 2. Parse network response ArrayList<Row>
rows( ) @heylaurakelly
1. JSON from server JSON 2. Parse network response 3.
Parse row subtypes ArrayList<Row> rows( ) LinkRow MapRow TitleRow HostAvatarRow @heylaurakelly
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
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
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
@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
@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
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
@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
@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
@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
@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
@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
@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
@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
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
Render with Epoxy HTTPS://GITHUB.COM/AIRBNB/EPOXY Open source library for building complex
screens in a RecyclerView
@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
@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
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
{ "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
How we’re using server-driven systems across Airbnb @heylaurakelly
@heylaurakelly
Building forms is simple, right? @heylaurakelly
What’s so hard about building a user flow? @heylaurakelly
What’s so hard about building a user flow? @heylaurakelly
What’s so hard about building a user flow? @heylaurakelly
What’s so hard about building a user flow? @heylaurakelly
@heylaurakelly Client-side logic
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
Hide and show steps based on state @heylaurakelly
Lona Dynamic UI @heylaurakelly
• 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
Think you want to try it? @heylaurakelly
• 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
@heylaurakelly
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