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. None
  2. None
  3. @jeremybbonze Android Engineer at WorkWell @xgouchet Lead Android Engineer at

    WorkWell
  4. None
  5. None
  6. None
  7. None
  8. None
  9. None
  10. None
  11. “Do not treat Offline as an error condition. Offline is

    the default state, Online is icing on the cake.”
  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.
  13. None
  14. None
  15. None
  16. None
  17. None
  18. None
  19. None
  20. None
  21. None
  22. None
  23. None
  24. None
  25. data class Item ( // … val lastModified: Long, val

    timeToLive: Long )
  26. 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> }
  27. 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> }
  28. 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") } }
  29. 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> }
  30. fun <O> Observable<LocalResult<O>>.flatMapOfflineFirst( fallback: RemoteSource.() -> Observable<O>, update: LocalSink.(O) ->

    Observable<O> ): Observable<O> { return flatMap { result -> when (result) { // … } } }
  31. return flatMap { result -> when (result) { is LocalResult.Present

    -> Observable.just(result.get()) // … } }
  32. 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()) } } // … } }
  33. return flatMap { result -> when (result) { // …

    is LocalResult.NotFound -> { if (networkWatcher.isNetworkAvailable()) { remoteSource.fallback() .flatMap { localSink.update(it) } } else { Observable.empty() } } } }
  34. fun <T : Any> List<LocalResult<T>>.keepPresentOnly() : LocalResult<List<T>> { return LocalResult.Present(

    filter { it is LocalResult.Present } .map { it.get() } ) }
  35. fun <T : Any> LocalResult<List<T>>.notFountIfEmpty() : LocalResult<List<T>> { return if

    (this is LocalResult.NotFound || get().isNotEmpty()) { This } else { LocalResult.NotFound() } }
  36. 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() } }) } }
  37. None
  38. None
  39. None
  40. 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() } } }
  41. 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 }
  42. • Same pagination logic on Remote and Local source

  43. None
  44. • Balance between disk usage / available data • Only

    deals with part of the problem
  45. • They do actually mean something • 404 (NOT FOUND)

    or 410 (GONE) → delete locally
  46. • Request (once in a while) recently deleted items •

    Use a Hash / Timestamp to only get the relevant changeset
  47. None
  48. • The main solution to handle offline actions is to

    schedule jobs
  49. • JobScheduler • Android-Job (Evernote) • JobDispatcher (Firebase) [Deprecated] •

    WorkManager
  50. • Set syncStatus in local DB • Launch sync job

    as an internet connection is back • Once remote and local DB are synced, update syncStatus
  51. data class Item ( // … val lastModified: Long, val

    timeToLive: Long, val syncStatus: SyncStatus ) enum class SyncStatus { TO_BE_SYNCED, SYNCED }
  52. None
  53. None
  54. None
  55. “Do not treat Offline as an error condition. Offline is

    the default state, Online is icing on the cake.”
  56. • Measure your network usage • Test your own application

    with lousy network conditions • Test your own application in airplane mode • Use your own application
  57. • Offline First Slack Community ◦ http://offlinefirst.org/chat/ • Offline resources

    and articles ◦ https://medium.com/offline-camp/ ◦ http://offlinefirst.org/
  58. None