Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Sign up for free
Menu
Search
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Pricing
Search
Sign in
Sign up for free
Laura Kelly - Rapidly iterating across platform...
Search
droidcon Berlin
July 17, 2018
Programming
130
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
89
Michael Jess - Enabling enterprise mobility with SAP
droidcon_berlin_2018
0
140
Ronen Sabag - Lean async code with Kotlin’s coroutines
droidcon_berlin_2018
0
92
Boris Farber & Nikita Kozlov - The_Build_Side_of_Android_App
droidcon_berlin_2018
0
230
Zan Markan - The state of Kotlin
droidcon_berlin_2018
0
90
Miquel Beltran - No More □ (tofu) Mastering Emoji on Android
droidcon_berlin_2018
0
150
Laurent Gasser & Jeremy Rochot - Sharing a success story - A low cost, Customer driven and co-developed Android EMM
droidcon_berlin_2018
0
340
Hoi Lam - Adding ML Kit to Android Things And some TensorFlow things
droidcon_berlin_2018
1
250
Other Decks in Programming
See All in Programming
Herb in Rails 8.2: Your ERB views, now HTML-aware @ Rails World 2026, Austin, Texas
marcoroth
0
220
TechTown - Creating Composable Callables in Contemporary C++
rollbear
0
130
作れる、でも消せるか?──安全に「枯らして消す」機能削除術 / Drain, Then Delete
chobishiba
2
670
phperだってパンチカードでコーディングしたい
o0h
PRO
0
340
見えないものを探る要求要件定義に必要な基本的思考 / invisible-requirement-thinking
minodriven
15
8.5k
Event is what happens
moro
2
780
半永久的に提供し続けられるプライベートクラウドを目指して ― 利用者の認知負荷を抑えるAPI抽象化とハードウェア世代交代の基盤設計
tomokon
0
500
最近”作らせてる” ローカルAIアプリの話
akit37
0
470
HTML-in-Canvasで作る未来のWeb UI表現
clockmaker
5
5.1k
AIで開発が速くなった先で、開発チームは何を目指すか
shihochan
0
1.3k
When benchmarks go bad - what I learned from measuring performance wrong
hollycummins
0
220
Agents on Rails - Rails at Scale 2026
irinanazarova
0
560
Featured
See All Featured
Practical Orchestrator
shlominoach
192
12k
The Director’s Chair: Orchestrating AI for Truly Effective Learning
tmiket
1
330
From π to Pie charts
rasagy
1
400
A better future with KSS
kneath
240
18k
It's Worth the Effort
3n
188
29k
Thoughts on Productivity
jonyablonski
76
5.4k
The Hidden Cost of Media on the Web [PixelPalooza 2025]
tammyeverts
2
530
Building Adaptive Systems
keathley
44
3.2k
Accessibility Awareness
sabderemane
1
250
The Mindset for Success: Future Career Progression
greggifford
PRO
0
540
Redefining SEO in the New Era of Traffic Generation
szymonslowik
1
480
BBQ
matthewcrist
90
10k
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