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
86
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
90
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
88
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
Ghostty + Neovimで作る 透明でカッコ良い開発環境
j341nono
0
130
更なる可用性を求めて、5年間運用したKotlinのアプリケーションをGoでリプレイスする話
ken_tunc
0
390
AGENTS.md Is Not Enough:Build Skills, Don't Download Them
lx_t
0
130
AWSに止められる覚悟してますか?
morizo_1984
2
460
iOSDC2026登壇資料.pdf
riofujimon
0
200
Snowflakeで業務アプリを作ろう。 Snowflakeのアプリ機能解説&実践ガイド
ayumu_yamaguchi
2
310
AHC070解法紹介
eijirou
0
140
選挙速報を多くのユーザーへ 届ける Live Activities 設計
hamayokokuririn
0
180
速習iPhone Duo対応
yuukiw00w
2
850
JPUG勉強会 OSSデータベースの内部構造を理解しよう(第2回)
oga5
0
280
難しいけど、読めた。- OSSの入口に立った話。
sts11142
0
130
巨大モノリシックアプリ モダン化大作戦
ktcryomm
1
1.1k
Featured
See All Featured
XXLCSS - How to scale CSS and keep your sanity
sugarenia
250
1.3M
Jamie Indigo - Trashchat’s Guide to Black Boxes: Technical SEO Tactics for LLMs
techseoconnect
PRO
0
680
The Pragmatic Product Professional
lauravandoore
37
7.5k
Stewardship and Sustainability of Urban and Community Forests
pwiseman
0
530
First, design no harm
axbom
PRO
2
1.3k
Evolution of real-time – Irina Nazarova, EuRuKo, 2024
irinanazarova
9
1.6k
JavaScript: Past, Present, and Future - NDC Porto 2020
reverentgeek
52
6.1k
Effective software design: The role of men in debugging patriarchy in IT @ Voxxed Days AMS
baasie
1
540
Darren the Foodie - Storyboard
khoart
PRO
4
4k
Making the Leap to Tech Lead
cromwellryan
135
10k
The Power of CSS Pseudo Elements
geoffreycrofte
82
6.6k
Un-Boring Meetings
codingconduct
0
430
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