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

Make your app work perfectly off— Connection Interrupted

Make your app work perfectly off— Connection Interrupted

An introduction to offline first development on Android, given with Jérémy Bartolomeu Bonze

Xavier Gouchet

April 24, 2019
Tweet

More Decks by Xavier Gouchet

Other Decks in Programming

Transcript

  1. View Slide

  2. View Slide

  3. @jeremybbonze
    Android Engineer at WorkWell
    @xgouchet
    Lead Android Engineer at WorkWell

    View Slide

  4. View Slide

  5. View Slide

  6. View Slide

  7. View Slide

  8. View Slide

  9. View Slide

  10. View Slide

  11. “Do not treat Offline as an error condition.
    Offline is the default state, Online is icing on the cake.”

    View Slide

  12. accessibility /əksɛsɪˈbɪlɪti/ n.
    ● the quality of being able to be reached or entered.
    ● the quality of being easy to obtain or use.

    View Slide

  13. View Slide

  14. View Slide

  15. View Slide

  16. View Slide

  17. View Slide

  18. View Slide

  19. View Slide

  20. View Slide


  21. View Slide


  22. View Slide

  23. View Slide


  24. View Slide

  25. View Slide

  26. View Slide


  27. View Slide


  28. View Slide

  29. View Slide

  30. data class Item (
    // …
    val lastModified: Long,
    val timeToLive: Long
    )

    View Slide

  31. interface ItemRemoteSource {
    fun get(id: String): Single
    fun getAll(): Single>
    }
    interface ItemRemoteSink {
    fun create(item: Item): Single
    fun update(item: Item): Single
    fun delete(id: String): Single
    }

    View Slide

  32. interface ItemLocalSource {
    fun get(id: String): Single>
    fun getAll(): Single>>
    }
    interface ItemLocalSink {
    fun create(item: Item): Single
    fun update(item: Item): Single
    fun delete(id: String): Single
    }

    View Slide

  33. sealed class LocalResult {
    abstract fun get(): T
    class Present(private val value: T): LocalResult() {
    override fun get(): T = value
    }
    class Obsolete(private val value: T): LocalResult() {
    override fun get(): T = value
    }
    class NotFound: LocalResult() {
    override fun get(): T =
    throw Resources.NotFoundException("No value")
    }
    }

    View Slide

  34. interface ItemGlobalSource {
    fun get(id: String): Single
    fun getAll(): Single>
    }
    interface ItemGlobalSink {
    fun create(item: Item): Single
    fun update(item: Item): Single
    fun delete(id: String): Single
    }

    View Slide

  35. fun Observable>.flatMapOfflineFirst(
    fallback: RemoteSource.() -> Observable,
    update: LocalSink.(O) -> Observable
    ): Observable {
    return flatMap { result ->
    when (result) {
    // …
    }
    }
    }

    View Slide

  36. return flatMap { result ->
    when (result) {
    is LocalResult.Present -> Observable.just(result.get())
    // …
    }
    }

    View Slide

  37. return flatMap { result ->
    when (result) {
    is LocalResult.Obsolete -> {
    if (networkWatcher.isNetworkAvailable()) {
    remoteSource.fallback()
    .flatMap { localSink.update(it) }
    .startWith(result.get())
    } else {
    Observable.just(result.get())
    }
    }
    // …
    }
    }

    View Slide

  38. return flatMap { result ->
    when (result) {
    // …
    is LocalResult.NotFound -> {
    if (networkWatcher.isNetworkAvailable()) {
    remoteSource.fallback()
    .flatMap { localSink.update(it) }
    } else {
    Observable.empty()
    }
    }
    }
    }

    View Slide

  39. fun List>.keepPresentOnly()
    : LocalResult> {
    return LocalResult.Present(
    filter { it is LocalResult.Present }
    .map { it.get() }
    )
    }

    View Slide

  40. fun LocalResult>.notFountIfEmpty()
    : LocalResult> {
    return if (this is LocalResult.NotFound || get().isNotEmpty()) {
    This
    } else {
    LocalResult.NotFound()
    }
    }

    View Slide

  41. fun List>.allPresentOrNone()
    : LocalResult> {
    return if (isNotEmpty() && all { it is LocalResult.Present }) {
    LocalResult.Present(map { it.get() })
    } else {
    LocalResult.Obsolete (mapNotNull {
    if (it is LocalResult.NotFound) {
    null
    } else {
    it.get()
    }
    })
    }
    }

    View Slide

  42. View Slide

  43. View Slide

  44. View Slide

  45. abstract class PaginationScrollListener(
    private val srLayout: SwipeRefreshLayout,
    private val onLoadNextPage: () -> Unit
    ) : RecyclerView.OnScrollListener() {
    override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
    super.onScrolled(recyclerView, dx, dy)
    if (isLastItemVisible() && !isFirstItemVisible()
    && !srLayout.isRefreshing) {
    onLoadNextPage()
    }
    }
    }

    View Slide

  46. override fun loadNextPage(context: ContextC, currentListSize: Int) {
    val pageToLoad = computePageToLoad(currentListSize)
    if (pageToLoad >= 0) {
    loadPage(context, pageToLoad, false)
    }
    }
    private fun computePageToLoad(currentListSize: Int): Int {
    val remainder = currentListSize % PAGE_SIZE
    val pageCount = (currentListSize - remainder) / PAGE_SIZE
    return if (remainder > 0) -1 else pageCount + 1
    }

    View Slide

  47. ● Same pagination logic on Remote and Local source

    View Slide

  48. View Slide

  49. ● Balance between disk usage / available data
    ● Only deals with part of the problem

    View Slide

  50. ● They do actually mean something
    ● 404 (NOT FOUND) or 410 (GONE) → delete locally

    View Slide

  51. ● Request (once in a while) recently deleted items
    ● Use a Hash / Timestamp to only get the relevant changeset

    View Slide

  52. View Slide

  53. ● The main solution to handle offline actions is to schedule jobs

    View Slide

  54. ● JobScheduler
    ● Android-Job (Evernote)
    ● JobDispatcher (Firebase) [Deprecated]
    ● WorkManager

    View Slide

  55. ● Set syncStatus in local DB
    ● Launch sync job as an internet connection is back
    ● Once remote and local DB are synced, update syncStatus

    View Slide

  56. data class Item (
    // …
    val lastModified: Long,
    val timeToLive: Long,
    val syncStatus: SyncStatus
    )
    enum class SyncStatus {
    TO_BE_SYNCED, SYNCED
    }

    View Slide

  57. View Slide

  58. View Slide


  59. View Slide

  60. View Slide

  61. “Do not treat Offline as an error condition.
    Offline is the default state, Online is icing on the cake.”

    View Slide

  62. ● Measure your network usage
    ● Test your own application with lousy network conditions
    ● Test your own application in airplane mode
    ● Use your own application

    View Slide

  63. ● Offline First Slack Community
    ○ http://offlinefirst.org/chat/
    ● Offline resources and articles
    ○ https://medium.com/offline-camp/
    ○ http://offlinefirst.org/

    View Slide

  64. View Slide