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

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>> }