1

TL;DR Looking for recommendations on robust offline support using rx-kotlin.

I've followed a nice guide on offline-support in Android apps. It works like this:

  1. Load data from the Internet, go to step 3 if error
  2. Store the data in the local database
  3. Load the data from the local database
  4. Display the data in the UI

The code is:

 Observable.mergeDelayError(
                loadRemoteData()
                    .doOnNext { writeDataToLocalDatabase(it) }
                    .subscribeOn(Schedulers.io()), 
                loadDataFromLocalDatabase()
                    .subscribeOn(Schedulers.io())
            )

Unfortunately, this approach relies on the database code always working. If the database operations for some reason fail, everything fails, even when the data is loaded successfully from the remote server.

Is there a way I can achieve the following using rx-kotlin/rx-java?:

  1. Load data from the Internet, go to step 3 if error
  2. Store the data in the local database
  3. Load the data from the local database
  4. (if steps 2 or 3 failed) Use the data from step 1
  5. Display the data in the UI

I'd like to avoid loading the data from the Internet twice. I'm using room + retrofit, if that matters.

EDIT: Thanks to @MinseongPark, I ended up with the code below. The mergeDelayError reports an error only if both the remote and the local source fails, and if the writeDataToLocalDatabase method fails (throws an Exception), then that does not keep the remote data from being reported to the UI. The information about errors in writeDataToLocalDatabase is saved by reporting it remotely. Now, the code is robust to one of the two sources failing, and to writing new entries to the database failing.

        return Observable.mergeDelayError(
            loadRemoteData().doOnNext {
                try {
                    writeDataToLocalDatabase(it)
                } catch (error: Throwable) {
                    Timber.d(error)
                    Crashlytics.logException(error)
                }
            },
            loadDataFromLocalDatabase()
        )
            .subscribeOn(Schedulers.io())
Per Christian Henden
  • 1,514
  • 1
  • 16
  • 26
  • what kind of db implementation are you using? On SQLite, you can (and should) start the transaction right before you're about to save the data. Once the data is saved you should tell the db that the data got saved successfully, and then you should end the transaction. If you don't end the transaction, the data doesn't get saved...therefore sending you to step 1 again. Does that help? – TooManyEduardos Dec 29 '19 at 23:24
  • I'm looking to cover all error cases, not just failure to commit data to the db. Transactions are generally a good practice, of course. Thanks for offering your suggestion :) – Per Christian Henden Dec 30 '19 at 10:19
  • so, you could throw an exception from the db (either at reading or writing), but AFAIK your rx chain can only have 1 `doOnError` call...which means that if there's an unrecoverable issue in the db processes your app will stay in a perpetual loop trying to download data and read/write that data to the db. You'll probably need some kind of counter or `retry(3)`, and then bail out with `onErrorResumeNext` – TooManyEduardos Dec 31 '19 at 04:02

1 Answers1

0

Try this.

    Observable.mergeDelayError(
        loadRemoteData()
            .doOnNext { runCatching { writeDataToLocalDatabase(it) } }
            .subscribeOn(Schedulers.io()),
        loadDataFromLocalDatabase()
            .onErrorResumeNext(Observable.empty())
            .subscribeOn(Schedulers.io())

    )
MinseongPark
  • 178
  • 6
  • Thanks, that brings me a bit closer! Exceptions from writeDataToLocalDatabase are covered by this approach, but exceptions from loadDataFromLocalDatabase are not. – Per Christian Henden Dec 30 '19 at 10:24
  • 1
    You can see this: `.onErrorResumeNext(Observable.empty())` . It'll convert empty stream if there's an exception at `loadDataFromLocalDatabase()`. If you still bothered with this code, please describe the full code of `loadDataFromLocalDatabase()` and exception trace. – MinseongPark Dec 30 '19 at 22:23
  • Sorry for by mistake switching the method names in my comment to you. Generally, I'm looking to be robust for exceptions thrown by **both** the mentioned two methods. A simple solution, I guess, is to wrap the doOnNext block with try/catch or runCatching:) If both sources fail here, I'll need the error to be reported. – Per Christian Henden Dec 31 '19 at 14:59