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

Cf95f93e78f6d6dd0630049396f723c6?s=128

Xavier Gouchet

April 24, 2019
Tweet

Transcript

  1. 1.
  2. 2.
  3. 4.
  4. 5.
  5. 6.
  6. 7.
  7. 8.
  8. 9.
  9. 10.
  10. 11.

    “Do not treat Offline as an error condition. Offline is

    the default state, Online is icing on the cake.”
  11. 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.
  12. 13.
  13. 14.
  14. 15.
  15. 16.
  16. 17.
  17. 18.
  18. 19.
  19. 20.
  20. 21.

  21. 22.

  22. 23.
  23. 24.

  24. 25.
  25. 26.
  26. 27.

  27. 28.

  28. 29.
  29. 31.

    interface ItemRemoteSource { fun get(id: String): Single<Item> fun getAll(): Single<List<Item>>

    } interface ItemRemoteSink { fun create(item: Item): Single<Item> fun update(item: Item): Single<Item> fun delete(id: String): Single<Boolean> }
  30. 32.

    interface ItemLocalSource { fun get(id: String): Single<LocalResult<Item>> fun getAll(): Single<List<LocalResult<Item>>>

    } interface ItemLocalSink { fun create(item: Item): Single<Item> fun update(item: Item): Single<Item> fun delete(id: String): Single<Boolean> }
  31. 33.

    sealed class LocalResult<T> { abstract fun get(): T class Present<T>(private

    val value: T): LocalResult<T>() { override fun get(): T = value } class Obsolete<T>(private val value: T): LocalResult<T>() { override fun get(): T = value } class NotFound<T>: LocalResult<T>() { override fun get(): T = throw Resources.NotFoundException("No value") } }
  32. 34.

    interface ItemGlobalSource { fun get(id: String): Single<Item> fun getAll(): Single<List<Item>>

    } interface ItemGlobalSink { fun create(item: Item): Single<Item> fun update(item: Item): Single<Item> fun delete(id: String): Single<Boolean> }
  33. 35.

    fun <O> Observable<LocalResult<O>>.flatMapOfflineFirst( fallback: RemoteSource.() -> Observable<O>, update: LocalSink.(O) ->

    Observable<O> ): Observable<O> { return flatMap { result -> when (result) { // … } } }
  34. 36.

    return flatMap { result -> when (result) { is LocalResult.Present

    -> Observable.just(result.get()) // … } }
  35. 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()) } } // … } }
  36. 38.

    return flatMap { result -> when (result) { // …

    is LocalResult.NotFound -> { if (networkWatcher.isNetworkAvailable()) { remoteSource.fallback() .flatMap { localSink.update(it) } } else { Observable.empty() } } } }
  37. 40.

    fun <T : Any> LocalResult<List<T>>.notFountIfEmpty() : LocalResult<List<T>> { return if

    (this is LocalResult.NotFound || get().isNotEmpty()) { This } else { LocalResult.NotFound() } }
  38. 41.

    fun <T : Any> List<LocalResult<T>>.allPresentOrNone() : LocalResult<List<T>> { 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() } }) } }
  39. 42.
  40. 43.
  41. 44.
  42. 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() } } }
  43. 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 }
  44. 48.
  45. 50.
  46. 51.

    • Request (once in a while) recently deleted items •

    Use a Hash / Timestamp to only get the relevant changeset
  47. 52.
  48. 55.

    • Set syncStatus in local DB • Launch sync job

    as an internet connection is back • Once remote and local DB are synced, update syncStatus
  49. 56.

    data class Item ( // … val lastModified: Long, val

    timeToLive: Long, val syncStatus: SyncStatus ) enum class SyncStatus { TO_BE_SYNCED, SYNCED }
  50. 57.
  51. 58.
  52. 59.

  53. 60.
  54. 61.

    “Do not treat Offline as an error condition. Offline is

    the default state, Online is icing on the cake.”
  55. 62.

    • Measure your network usage • Test your own application

    with lousy network conditions • Test your own application in airplane mode • Use your own application
  56. 63.

    • Offline First Slack Community ◦ http://offlinefirst.org/chat/ • Offline resources

    and articles ◦ https://medium.com/offline-camp/ ◦ http://offlinefirst.org/
  57. 64.