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
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
0
210
Saket Narayan - Persistence as the single source of truth
droidcon Berlin
July 10, 2018
Tweet
Share
More Decks by droidcon Berlin
See All by droidcon Berlin
Jon Markoff - Best practice for apps
droidcon_berlin_2018
0
210
Jon Markoff - Voice in the enterprise
droidcon_berlin_2018
0
74
Michael Jess - Enabling enterprise mobility with SAP
droidcon_berlin_2018
0
120
Ronen Sabag - Lean async code with Kotlin’s coroutines
droidcon_berlin_2018
0
76
Boris Farber & Nikita Kozlov - The_Build_Side_of_Android_App
droidcon_berlin_2018
0
200
Zan Markan - The state of Kotlin
droidcon_berlin_2018
0
81
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
310
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
Go Conference mini in Sendai 2026 : Goに新機能を提案し実装されるまでのフロー徹底解説
yamatoya
0
610
Claude Codeログ基盤の構築
giginet
PRO
7
3.4k
ベクトル検索のフィルタを用いた機械学習モデルとの統合 / python-meetup-fukuoka-06-vector-attr
monochromegane
2
470
CSC307 Lecture 15
javiergs
PRO
0
260
「接続」—パフォーマンスチューニングの最後の一手 〜点と点を結ぶ、その一瞬のために〜
kentaroutakeda
1
120
Symfony + NelmioApiDocBundle を使った スキーマ駆動開発 / Schema Driven Development with NelmioApiDocBundle
okashoi
0
160
どんと来い、データベース信頼性エンジニアリング / Introduction to DBRE
nnaka2992
1
300
エンジニアの「手元の自動化」を加速するn8n 2026.02.27
symy2co
0
160
Linux Kernelの1文字のミスで 権限昇格ができた話
rqda
0
1.6k
Fundamentals of Software Engineering In the Age of AI
therealdanvega
2
260
AI時代のシステム設計:ドメインモデルで変更しやすさを守る設計戦略
masuda220
PRO
5
1.1k
PHP 7.4でもOpenTelemetryゼロコード計装がしたい! / PHPerKaigi 2026
arthur1
1
110
Featured
See All Featured
Collaborative Software Design: How to facilitate domain modelling decisions
baasie
0
160
Bash Introduction
62gerente
615
210k
Accessibility Awareness
sabderemane
0
82
Imperfection Machines: The Place of Print at Facebook
scottboms
269
14k
SEO for Brand Visibility & Recognition
aleyda
0
4.4k
個人開発の失敗を避けるイケてる考え方 / tips for indie hackers
panda_program
122
21k
DBのスキルで生き残る技術 - AI時代におけるテーブル設計の勘所
soudai
PRO
64
51k
The Psychology of Web Performance [Beyond Tellerrand 2023]
tammyeverts
49
3.3k
Darren the Foodie - Storyboard
khoart
PRO
3
2.9k
Making Projects Easy
brettharned
120
6.6k
Efficient Content Optimization with Google Search Console & Apps Script
katarinadahlin
PRO
1
410
Practical Tips for Bootstrapping Information Extraction Pipelines
honnibal
25
1.8k
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]