Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Saket Narayan - Persistence as the single sourc...

Saket Narayan - Persistence as the single source of truth

Avatar for droidcon Berlin

droidcon Berlin

July 10, 2018
Tweet

More Decks by droidcon Berlin

Other Decks in Programming

Transcript

  1. The best kind of apps • never make the user

    wait • avoid blocking progress indicators • forgive bad data connectivity
  2. The usual way fun onSubmit() { api.login(username, password) .subscribe {

    authToken -> database.save(authToken) startActivity(HomeScreen.intent()) } }
  3. The usual way fun onSubmit() { api.login(username, password) .subscribe {

    authToken -> database.save(authToken) startActivity(HomeScreen.intent()) } }
  4. The usual way fun onSubmit() { api.login(username, password) .subscribe {

    authToken -> database.save(authToken) startActivity(HomeScreen.intent()) } }
  5. The usual way fun onSubmit() { api.login(username, password) .subscribe {

    authToken -> database.save(authToken) startActivity(HomeScreen.intent()) } }
  6. What if the Activity gets destroyed? fun onSubmit() { api.login(username,

    password) .subscribe { authToken -> database.save(authToken) startActivity(HomeScreen.intent()) } }
  7. What if the Activity gets destroyed? fun onSubmit() { api.login(username,

    password) .takeUntil(lifecycle.onDestroy) .subscribe { authToken -> database.save(authToken) startActivity(HomeScreen.intent()) } }
  8. The PSST way fun onSubmit() { api.login(username, password) .subscribe {

    authToken -> database.save(authToken) startActivity(HomeScreen.intent()) } }
  9. The PSST way fun onSubmit() { api.login(username, password) .subscribe {

    authToken -> database.save(authToken) startActivity(HomeScreen.intent()) } }
  10. The PSST way class AuthRepo { fun login(name: String, pass:

    String) { api.login(name, pass) .subscribe { authToken -> database.save(authToken) } } }
  11. The PSST way class AuthRepo { fun login(name: String, pass:

    String) { api.login(name, pass) .subscribe { authToken -> database.save(authToken) } } }
  12. 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()
  13. 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() } } }
  14. 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>
  15. 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() } } }
  16. 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() } } }
  17. 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() } } }
  18. 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() } } }
  19. The usual way class CommentsScreen : Activity { fun onCreate()

    { val submission = intent.getParcelable("submission") nameView.text = submission.name votesView.text = "${submission.likes} votes" } }
  20. The usual way class CommentsScreen : Activity { fun onCreate()

    { val submission = intent.getParcelable("submission") nameView.text = submission.name votesView.text = "${submission.likes} votes” } }
  21. The usual way class CommentsScreen : Activity { fun onCreate()

    { val submission = intent.getParcelable("submission") nameView.text = submission.name votesView.text = "${submission.likes} votes" } }
  22. The usual way class CommentsScreen : Activity { fun onVoteClick()

    { showProgressDialog() api.upvote(submission.id) .subscribe { newVoteCount -> hideProgressDialog() votesView.text = "$newVoteCount votes” database.save(newVoteCount) } } }
  23. The usual way class CommentsScreen : Activity { fun onVoteClick()

    { showProgressDialog() api.upvote(submission.id) .subscribe { newVoteCount -> hideProgressDialog() votesView.text = "$newVoteCount votes” database.save(newVoteCount) } } }
  24. The usual way class CommentsScreen : Activity { fun onVoteClick()

    { showProgressDialog() api.upvote(submission.id) .subscribe { newVoteCount -> hideProgressDialog() votesView.text = "$newVoteCount votes” database.save(newVoteCount) } } }
  25. The usual way class CommentsScreen : Activity { fun onVoteClick()

    { showProgressDialog() api.upvote(submission.id) .subscribe { newVoteCount -> hideProgressDialog() votesView.text = "$newVoteCount votes” database.save(newVoteCount) } } }
  26. The usual way class CommentsScreen : Activity { fun onVoteClick()

    { showProgressDialog() api.upvote(submission.id) .subscribe { newVoteCount -> hideProgressDialog() votesView.text = "$newVoteCount votes” database.save(newVoteCount) } } }
  27. The usual way class CommentsScreen : Activity { fun onVoteClick()

    { showProgressDialog() api.upvote(submission.id) .subscribe { newVoteCount -> hideProgressDialog() votesView.text = "$newVoteCount votes” database.save(newVoteCount) } } }
  28. Problems • UI is being updated in an ad-hoc manner

    fun onCreate() { votesView.text = “${submission.votes} votes" } fun onVoteClick() { votesView.text = "$newVoteCount votes" }
  29. Problems • UI is being updated in an ad-hoc manner

    • Needs synchronisation • Complexity compounds over time
  30. Problems • UI is being updated in an ad-hoc manner

    • Saving state manually is a pain
  31. 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") } }
  32. 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") } }
  33. 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") } }
  34. Problems • UI is being updated in an ad-hoc manner

    • Saving state manually is a pain
  35. The PSST way fun onCreate() { val submission = intent.getParcelable(“submission”)

    nameView.text = submission.name votesView.text = "${submission.likes} votes" }
  36. The PSST way fun onCreate() { val submission = intent.getParcelable(“submission”)

    nameView.text = submission.name votesView.text = "${submission.likes} votes" }
  37. The PSST way fun onCreate() { val submissionId = intent.getString(“submission_id")

    submissionRepo.submission(submissionId) .takeUntil(lifecycle.onDestroy) .subscribe { submission -> votesView.text = “${submission.votes} votes” } }
  38. 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>
  39. The PSST way fun onCreate() { val submissionId = intent.getString(“submission_id")

    submissionRepo.submission(submissionId) .takeUntil(lifecycle.onDestroy) .subscribe { submission -> votesView.text = “${submission.votes} votes” } }
  40. 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()
  41. 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) }
  42. 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) }
  43. 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) }
  44. class SubmissionRepo { fun submission(id: String): Observable<Submission> { return database.comments(id)

    } fun vote(id: String) { api.vote(id) .subscribe { updatedSubmission -> database.save(updatedSubmission) } } }
  45. class SubmissionRepo { fun submission(id: String): Observable<Submission> { return database.comments(id)

    } fun vote(id: String) { api.vote(id) .subscribe { updatedSubmission -> database.save(updatedSubmission) } } }
  46. class SubmissionRepo { fun submission(id: String): Observable<Submission> { return database.comments(id)

    } fun vote(id: String) { api.vote(id) .subscribe { updatedSubmission -> database.save(updatedSubmission) } } }
  47. Obvious wins • UI is driven from a single source

    of truth • Passing serialized data is not required • UI automatically receives updates
  48. 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
  49. class SubmissionRepo { fun vote(id: String) { api.vote(id) .subscribe {

    updatedSubmission -> database.save(updatedSubmission) } } }
  50. class SubmissionRepo { fun vote(id: String) { // Assume that

    the network call will succeed. api.vote(id) .subscribe { updatedSubmission -> database.save(updatedSubmission) } } }
  51. 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() } }
  52. 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() } }
  53. 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() } }
  54. Obvious wins • UI is driven from a single source

    of truth • Passing serialized data is not required • UI automatically receives updates
  55. 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
  56. If a network call does not involve any extra server

    validation, assume that it’ll succeed.
  57. class CommentsScreen { fun onCreate() { val submissionId = intent.getString("id")

    submissionRepo.submission(submissionId) .takeUntil(lifecycle.onDestroy) .subscribe { populateUi(it) } } }
  58. class CommentsScreen { fun onCreate() { val submissionId = intent.getString("id")

    submissionRepo.submission(submissionId) .takeUntil(lifecycle.onDestroy) .subscribe { populateUi(it) } } }
  59. class CommentsScreen { fun onCreate() { val submissionId = intent.getString("id")

    submissionRepo.submission(submissionId) .takeUntil(lifecycle.onDestroy) .subscribe { populateUi(it) } } }
  60. class CommentsScreen { fun onCreate() { val submissionId = intent.getString("id")

    submissionRepo.submission(submissionId) .takeUntil(lifecycle.onDestroy) .subscribe { populateUi(it) } } }
  61. 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) } }
  62. 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) } }
  63. 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)) } } }
  64. 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)) } } }
  65. 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)) } } }
  66. 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)) } } }
  67. 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)) } } }
  68. 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" } } }
  69. 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" } } }
  70. 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" } } }
  71. 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" } } }
  72. 1) SQLite is time consuming • True, plain SQLite requires

    a lot of boilerplate • Use Room instead
  73. Room is easy @Entity data class Comment(body: String, status: SyncStatus)

    @Dao interface CommentDao { @Query("SELECT * FROM comment") fun comments(): Flowable<List<Comment>> }
  74. Room is easy @Entity data class Comment(body: String, status: SyncStatus)

    @Dao interface CommentDao { @Query("SELECT * FROM comment") fun comments(): Flowable<List<Comment>> }