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
Saket Narayan - Persistence as the single sourc...
Search
Sponsored
·
Your Podcast. Everywhere. Effortlessly.
Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.
→
droidcon Berlin
July 10, 2018
Programming
230
0
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
Saket Narayan - Persistence as the single source of truth
droidcon Berlin
July 10, 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
89
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
FreeBSDでZabbixを動かす.pdf
kenkino
0
120
App Storeの外へ──日本のiOSサイドローディング入門 for iOSDC Japan 2026
yuukiw00w
0
200
MVNOの申込からeSIM開通までをiOSアプリでつなぐ- 本人確認・MNP・通信事業者基盤をまたぐ実装
satotakeshi
0
160
一参加者から『中の人』へ 〜全通PHPerがブースに立って学んだ、カンファレンスを100倍楽しむコツ〜
wp_daisuke
0
150
新人はどこまで自力でやり、どこからAIに頼るべきか/エンジニア育成に向き合う_先輩たちの悩みと知見共有会
toppan_digital_dev
1
610
AHC070解法紹介
eijirou
0
110
Deep dive into the select statement (GopherCon UK)
jespino
0
180
How I Stole PSI from Android Studio - DroidKaigi2026
worker8
0
120
業務時間外もAIに働いてもらう話
colorful12
3
10k
Can LLMs Replicate 4 Years of Compose Migration? Exploring the boundaries of automation with 279 XML files from a real product
makun
0
140
Security issues being discussed on Web Platforms
petamoriken
0
880
海上で動くGoサーバー: goroutineとchannelでさばく航行データストリーム
atsuki_seo
0
120
Featured
See All Featured
Claude Code のすすめ
schroneko
67
230k
What’s in a name? Adding method to the madness
productmarketing
PRO
24
4.2k
Git: the NoSQL Database
bkeepers
PRO
432
67k
Amusing Abliteration
ianozsvald
1
300
Building the Perfect Custom Keyboard
takai
2
870
Bootstrapping a Software Product
garrettdimon
PRO
306
120k
Typedesign – Prime Four
hannesfritz
42
3.2k
The agentic SEO stack - context over prompts
schlessera
0
930
How to Align SEO within the Product Triangle To Get Buy-In & Support - #RIMC
aleyda
2
1.8k
Fireside Chat
paigeccino
43
4k
How to build a perfect <img>
jonoalderson
1
6k
Prompt Engineering for Job Search
mfonobong
0
450
Transcript
Persistence as the single source of truth uncommon.is Saket Narayan
Persistence as the single source of truth uncommon.is Saket Narayan
None
Offline first apps are the best kind of apps
The best kind of apps • never make the user
wait • avoid blocking progress indicators • forgive bad data connectivity
Fyi, offline first != apps designed for cities with bad
connectivity
Fyi, offline first != apps designed for cities with bad
connectivity
Bad connectivity is everywhere Source: Unsplash
Simplifying state management is the key to designing offline first
apps
Simplifying state management is the key to designing offline first
apps
But state management is difficult
None
Source: Jake Wharton
Source: Jake Wharton
@saketme Persistence as the single source of truth
@saketme Or, PSST
Some graphs to prove my point
Time Productivity
Time Productivity
Time Productivity
None
None
None
@saketme Usecases T minus 6
@saketme Usecase: Login screen T minus 5
The usual way fun onSubmit() { api.login(username, password) .subscribe {
authToken -> database.save(authToken) startActivity(HomeScreen.intent()) } }
The usual way fun onSubmit() { api.login(username, password) .subscribe {
authToken -> database.save(authToken) startActivity(HomeScreen.intent()) } }
The usual way fun onSubmit() { api.login(username, password) .subscribe {
authToken -> database.save(authToken) startActivity(HomeScreen.intent()) } }
The usual way fun onSubmit() { api.login(username, password) .subscribe {
authToken -> database.save(authToken) startActivity(HomeScreen.intent()) } }
What if the Activity gets destroyed? fun onSubmit() { api.login(username,
password) .subscribe { authToken -> database.save(authToken) startActivity(HomeScreen.intent()) } }
What if the Activity gets destroyed? fun onSubmit() { api.login(username,
password) .takeUntil(lifecycle.onDestroy) .subscribe { authToken -> database.save(authToken) startActivity(HomeScreen.intent()) } }
Problems • No support for orientation changes
AndroidManifest.xml <activity android:name=".LoginScreen" />
AndroidManifest.xml <activity android:name=".LoginScreen" android:screenOrientation="portrait" />
Problems • No support for orientation changes • Response may
be received while minimized
The PSST way fun onSubmit() { api.login(username, password) .subscribe {
authToken -> database.save(authToken) startActivity(HomeScreen.intent()) } }
The PSST way fun onSubmit() { api.login(username, password) .subscribe {
authToken -> database.save(authToken) startActivity(HomeScreen.intent()) } }
The PSST way fun onSubmit() { authRepo.login(username, password) }
The PSST way class AuthRepo { fun login(name: String, pass:
String) { api.login(name, pass) .subscribe { authToken -> database.save(authToken) } } }
The PSST way class AuthRepo { fun login(name: String, pass:
String) { api.login(name, pass) .subscribe { authToken -> database.save(authToken) } } }
The PSST way fun onSubmit() { authRepo.login(username, password) } fun
onStart() { authRepo.userSession() .takeUntil(lifecycle.onStop) .subscribe { -> when (it) -> { is Absent -> showLoginForm() is InFlight -> showProgress() is Present -> openNextActivity()
fun onSubmit() { authRepo.login(username, password) } fun onStart() { authRepo.userSession()
.takeUntil(lifecycle.onStop) .subscribe { -> when (it) -> { is Absent -> showLoginForm() is InFlight -> showProgress() is Present -> openNextActivity() } } }
fun onSubmit() { authRepo.login(username, password) } fun onStart() { authRepo.userSession()
.takeUntil(lifecycle.onStop) .subscribe { -> when (it) -> { is Absent -> showLoginForm() is InFlight -> showProgress() is Present -> openNextActivity() } } } Observable<UserSession>
fun onSubmit() { authRepo.login(username, password) } fun onStart() { authRepo.userSession()
.takeUntil(lifecycle.onStop) .subscribe { -> when (it) -> { is Absent -> showLoginForm() is InFlight -> showProgress() is Present -> openNextActivity() } } }
fun onSubmit() { authRepo.login(username, password) } fun onStart() { authRepo.userSession()
.takeUntil(lifecycle.onStop) .subscribe { -> when (it) -> { is Absent -> showLoginForm() is InFlight -> showProgress() is Present -> openNextActivity() } } }
fun onSubmit() { authRepo.login(username, password) } fun onStart() { authRepo.userSession()
.takeUntil(lifecycle.onStop) .subscribe { -> when (it) -> { is Absent -> showLoginForm() is InFlight -> showProgress() is Present -> openNextActivity() } } }
fun onSubmit() { authRepo.login(username, password) } fun onStart() { authRepo.userSession()
.takeUntil(lifecycle.onStop) .subscribe { -> when (it) -> { is Absent -> showLoginForm() is InFlight -> showProgress() is Present -> openNextActivity() } } }
@saketme Usecase: Content details screen T minus 4
None
The usual way val onClick: { submission -> startActivity( CommentsScreen.intent(this,
submission) ) }
The usual way class CommentsScreen : Activity { fun onCreate()
{ val submission = intent.getParcelable("submission") nameView.text = submission.name votesView.text = "${submission.likes} votes" } }
The usual way class CommentsScreen : Activity { fun onCreate()
{ val submission = intent.getParcelable("submission") nameView.text = submission.name votesView.text = "${submission.likes} votes” } }
The usual way class CommentsScreen : Activity { fun onCreate()
{ val submission = intent.getParcelable("submission") nameView.text = submission.name votesView.text = "${submission.likes} votes" } }
The usual way class CommentsScreen : Activity { fun onVoteClick()
{ showProgressDialog() api.upvote(submission.id) .subscribe { newVoteCount -> hideProgressDialog() votesView.text = "$newVoteCount votes” database.save(newVoteCount) } } }
The usual way class CommentsScreen : Activity { fun onVoteClick()
{ showProgressDialog() api.upvote(submission.id) .subscribe { newVoteCount -> hideProgressDialog() votesView.text = "$newVoteCount votes” database.save(newVoteCount) } } }
The usual way class CommentsScreen : Activity { fun onVoteClick()
{ showProgressDialog() api.upvote(submission.id) .subscribe { newVoteCount -> hideProgressDialog() votesView.text = "$newVoteCount votes” database.save(newVoteCount) } } }
The usual way class CommentsScreen : Activity { fun onVoteClick()
{ showProgressDialog() api.upvote(submission.id) .subscribe { newVoteCount -> hideProgressDialog() votesView.text = "$newVoteCount votes” database.save(newVoteCount) } } }
The usual way class CommentsScreen : Activity { fun onVoteClick()
{ showProgressDialog() api.upvote(submission.id) .subscribe { newVoteCount -> hideProgressDialog() votesView.text = "$newVoteCount votes” database.save(newVoteCount) } } }
The usual way class CommentsScreen : Activity { fun onVoteClick()
{ showProgressDialog() api.upvote(submission.id) .subscribe { newVoteCount -> hideProgressDialog() votesView.text = "$newVoteCount votes” database.save(newVoteCount) } } }
Problems • UI is being updated in an ad-hoc manner
Problems • UI is being updated in an ad-hoc manner
fun onCreate() { votesView.text = “${submission.votes} votes" } fun onVoteClick() { votesView.text = "$newVoteCount votes" }
Problems • UI is being updated in an ad-hoc manner
• Needs synchronisation • Complexity compounds over time
Problems • UI is being updated in an ad-hoc manner
• Saving state manually is a pain
Saving state? fun onSaveState(outState: Bundle) { outState.putParcelable("submission", submission) } fun
onCreate(savedState: Bundle) { val submission = when { savedState == null —> intent.getParcelableExtra("submission") else —> savedState.getParcelable(“submission") } }
Saving state? fun onSaveState(outState: Bundle) { outState.putParcelable("submission", submission) } fun
onCreate(savedState: Bundle) { val submission = when { savedState == null —> intent.getParcelableExtra("submission") else —> savedState.getParcelable(“submission") } }
Saving state? fun onSaveState(outState: Bundle) { outState.putParcelable("submission", submission) } fun
onCreate(savedState: Bundle) { val submission = when { savedState == null —> intent.getParcelableExtra("submission") else —> savedState.getParcelable(“submission") } }
Problems • UI is being updated in an ad-hoc manner
• Saving state manually is a pain
The PSST way val onClick: { movie -> startActivity( CommentsScreen.intent(this,
submission) ) }
The PSST way val onClick: { movie -> startActivity( CommentsScreen.intent(this,
submission.id) ) }
The PSST way val onClick: { movie -> startActivity( CommentsScreen.intent(this,
submission.id) ) }
The PSST way fun onCreate() { val submission = intent.getParcelable(“submission”)
nameView.text = submission.name votesView.text = "${submission.likes} votes" }
The PSST way fun onCreate() { val submission = intent.getParcelable(“submission”)
nameView.text = submission.name votesView.text = "${submission.likes} votes" }
The PSST way fun onCreate() { val submissionId = intent.getString(“submission_id”)
}
The PSST way fun onCreate() { val submissionId = intent.getString(“submission_id")
submissionRepo.submission(submissionId) .takeUntil(lifecycle.onDestroy) .subscribe { submission -> votesView.text = “${submission.votes} votes” } }
The PSST way fun onCreate() { val submissionId = intent.getString(“submission_id")
submissionRepo.submission(submissionId) .takeUntil(lifecycle.onDestroy) .subscribe { submission -> votesView.text = “${submission.votes} votes” } } Observable<Submission>
The PSST way fun onCreate() { val submissionId = intent.getString(“submission_id")
submissionRepo.submission(submissionId) .takeUntil(lifecycle.onDestroy) .subscribe { submission -> votesView.text = “${submission.votes} votes” } }
The PSST way fun onCreate() { val submissionId = intent.getString(“submission_id")
submissionRepo.submission(submissionId) .takeUntil(lifecycle.onDestroy) .subscribe { submission -> votesView.text = “${submission.votes} votes” } } fun onVoteClick() { showProgressDialog()
fun onVoteClick() { showProgressDialog() api.vote(submission.id) .subscribe { newVoteCount -> hideProgressDialog()
votesView.text = "$newVoteCount votes” } }
fun onVoteClick() { showProgressDialog() api.vote(submission.id) .subscribe { newVoteCount -> hideProgressDialog()
votesView.text = "$newVoteCount votes” } }
fun onVoteClick() { submissionRepo.vote(submission.id) }
fun onCreate() { val submissionId = intent.getString(“submission_id") submissionRepo.submission(submissionId) .takeUntil(lifecycle.onDestroy) .subscribe
{ submission -> votesView.text = “${submission.votes} votes” } } fun onVoteClick() { submissionRepo.vote(submission.id) }
fun onCreate() { val submissionId = intent.getString(“submission_id") submissionRepo.comments(submissionId) .takeUntil(lifecycle.onDestroy) .subscribe
{ submission -> votesView.text = “${submission.votes} votes” } } fun onVoteClick() { submissionRepo.vote(submission.id) }
fun onCreate() { val submissionId = intent.getString(“submission_id") submissionRepo.comments(submissionId) .takeUntil(lifecycle.onDestroy) .subscribe
{ submission -> votesView.text = “${submission.votes} votes” } } fun onVoteClick() { submissionRepo.vote(submission.id) }
class SubmissionRepo { fun submission(id: String): Observable<Submission> { return database.comments(id)
} fun vote(id: String) { api.vote(id) .subscribe { updatedSubmission -> database.save(updatedSubmission) } } }
class SubmissionRepo { fun submission(id: String): Observable<Submission> { return database.comments(id)
} fun vote(id: String) { api.vote(id) .subscribe { updatedSubmission -> database.save(updatedSubmission) } } }
class SubmissionRepo { fun submission(id: String): Observable<Submission> { return database.comments(id)
} fun vote(id: String) { api.vote(id) .subscribe { updatedSubmission -> database.save(updatedSubmission) } } }
Obvious wins • UI is driven from a single source
of truth • Passing serialized data is not required • UI automatically receives updates
Obvious wins • UI is driven from a single source
of truth • Passing serialized data is not required • UI automatically receives updates • Even when changes happen outside the screen. E.g., from a notification
Improving the UX
class SubmissionRepo { fun vote(id: String) { api.vote(id) .subscribe {
updatedSubmission -> database.save(updatedSubmission) } } }
class SubmissionRepo { fun vote(id: String) { // Assume that
the network call will succeed. api.vote(id) .subscribe { updatedSubmission -> database.save(updatedSubmission) } } }
class SubmissionRepo { fun vote(id: String) { // Assume that
the network call will succeed. val update = database.submission(id) .take(1) .map { it.incrementVotesBy(1) } .andThen { save(it) } val call = api.vote(id) .andThen { save(it) } update .andThen(call) .subscribe() } }
class SubmissionRepo { fun vote(id: String) { // Assume that
the network call will succeed. val update = database.submission(id) .take(1) .map { it.incrementVotesBy(1) } .andThen { save(it) } val call = api.vote(id) .andThen { save(it) } update .andThen(call) .subscribe() } }
class SubmissionRepo { fun vote(id: String) { // Assume that
the network call will succeed. val update = database.submission(id) .take(1) .map { it.incrementVotesBy(1) } .andThen { save(it) } val call = api.vote(id) .andThen { save(it) } update .andThen(call) .subscribe() } }
Obvious wins • UI is driven from a single source
of truth • Passing serialized data is not required • UI automatically receives updates
Obvious wins • UI is driven from a single source
of truth • Passing serialized data is not required • UI automatically receives updates • Ui receives immediate feedback
If a network call does not involve any extra server
validation, assume that it’ll succeed.
@saketme Usecase: Comments screen T minus 3
Relay Sync Flamingo
Relay Sync Flamingo
None
Relay Sync Flamingo
None
Relay Sync Flamingo
None
A better experience, is to remove the notion of network
from the user’s perspective
None
class CommentsScreen { fun onCreate() { val submissionId = intent.getString("id")
submissionRepo.submission(submissionId) .takeUntil(lifecycle.onDestroy) .subscribe { populateUi(it) } } }
class CommentsScreen { fun onCreate() { val submissionId = intent.getString("id")
submissionRepo.submission(submissionId) .takeUntil(lifecycle.onDestroy) .subscribe { populateUi(it) } } }
class CommentsScreen { fun onCreate() { val submissionId = intent.getString("id")
submissionRepo.submission(submissionId) .takeUntil(lifecycle.onDestroy) .subscribe { populateUi(it) } } }
class CommentsScreen { fun onCreate() { val submissionId = intent.getString("id")
submissionRepo.submission(submissionId) .takeUntil(lifecycle.onDestroy) .subscribe { populateUi(it) } } }
class CommentsScreen { fun onCreate() { val submissionId = intent.getString("id")
submissionRepo.submission(submissionId) .takeUntil(lifecycle.onDestroy) .subscribe { populateUi(it) } } fun onSendReplyClick(parent: Comment, body: String) { submissionRepo.reply(parent, body) } }
class CommentsScreen { fun onCreate() { val submissionId = intent.getString("id")
submissionRepo.submission(submissionId) .takeUntil(lifecycle.onDestroy) .subscribe { populateUi(it) } } fun onSendReplyClick(parent: Comment, body: String) { submissionRepo.reply(parent, body) } }
class SubmissionRepo { fun reply(parent: Comment, body: String) { api.reply(parent,
body) .map { SyncStatus.Synced() } .onError(SyncStatus.Failed()) .startWith(SyncStatus.InFlight()) .subscribe { status -> database.save(Comment(parent, body, status)) } } }
class SubmissionRepo { fun reply(parent: Comment, body: String) { api.reply(parent,
body) .map { SyncStatus.Synced() } .onError(SyncStatus.Failed()) .startWith(SyncStatus.InFlight()) .subscribe { status -> database.save(Comment(parent, body, status)) } } }
class SubmissionRepo { fun reply(parent: Comment, body: String) { api.reply(parent,
body) .map { SyncStatus.Synced() } .onError(SyncStatus.Failed()) .startWith(SyncStatus.InFlight()) .subscribe { status -> database.save(Comment(parent, body, status)) } } }
class SubmissionRepo { fun reply(parent: Comment, body: String) { api.reply(parent,
body) .map { SyncStatus.Synced() } .onError(SyncStatus.Failed()) .startWith(SyncStatus.InFlight()) .subscribe { status -> database.save(Comment(parent, body, status)) } } }
class SubmissionRepo { fun reply(parent: Comment, body: String) { api.reply(parent,
body) .map { SyncStatus.Synced() } .onError(SyncStatus.Failed()) .startWith(SyncStatus.InFlight()) .subscribe { status -> database.save(Comment(parent, body, status)) } } }
class CommentViewHolder { fun onBind(comment: Comment) { bylineView.text = when(comment.syncStatus)
{ is InFlight -> “Posting..." is Synced -> "Posted at ${comment.sentTime}" is Failed -> "Failed. Tap to retry" } } }
class CommentViewHolder { fun onBind(comment: Comment) { bylineView.text = when(comment.syncStatus)
{ is InFlight -> “Posting..." is Synced -> "Posted at ${comment.sentTime}" is Failed -> "Failed. Tap to retry" } } }
class CommentViewHolder { fun onBind(comment: Comment) { bylineView.text = when(comment.syncStatus)
{ is InFlight -> “Posting..." is Synced -> "Posted at ${comment.sentTime}" is Failed -> "Failed. Tap to retry" } } }
class CommentViewHolder { fun onBind(comment: Comment) { bylineView.text = when(comment.syncStatus)
{ is InFlight -> “Posting..." is Synced -> "Posted at ${comment.sentTime}" is Failed -> "Failed. Tap to retry" } } }
Re-sending failed comments class BootCompleteReceiver : Receiver() { fun onReceive()
{ submissionRepo.sendUnsyncedComments() } }
Re-sending failed comments class RedditApp : Application() { fun onCreate()
{ submissionRepo.sendUnsyncedComments() } }
Re-sending failed comments class ReplyWorker : Worker() { fun doWork()
{ submissionRepo.sendUnsyncedComments() } }
@saketme Common excuses for avoiding Persistence T minus 2
1) SQLite is time consuming
1) SQLite is time consuming • True, plain SQLite requires
a lot of boilerplate
None
1) SQLite is time consuming • True, plain SQLite requires
a lot of boilerplate
1) SQLite is time consuming • True, plain SQLite requires
a lot of boilerplate • Use Room instead
Room is easy data class Comment(body: String, status: SyncStatus)
Room is easy @Entity data class Comment(body: String, status: SyncStatus)
Room is easy @Entity data class Comment(body: String, status: SyncStatus)
@Dao interface CommentDao { @Query("SELECT * FROM comment") fun comments(): Flowable<List<Comment>> }
Room is easy @Entity data class Comment(body: String, status: SyncStatus)
@Dao interface CommentDao { @Query("SELECT * FROM comment") fun comments(): Flowable<List<Comment>> }
2) Reading from disk is slow
2) Reading from disk is slow • No
2) Reading from disk is slow • No • Activities
take ~250ms to animate in
Persistence != SharedPreferences
@saketme PSST: Wrap up T minus 1
With PSST, State = representation of persisted data
With PSST, UI reacts to a single source of data
changes
With PSST, Support for config changes and app deaths for
free
With PSST, Offline first design out of the box
With PSST, Increased perceived speed of the app
@saketme Store github.com/NYTimes/Store T minus 0
@saketme onComplete()
[email protected]