3

Here is my use case:

I am developing an app that communicates with a server via a REST API and stores the received data in a SQLite database (it's using it as a cache of some sorts).

When the user opens a screen, the following has to occur:

  1. The data is loaded from the DB, if available.
  2. The app call the API to refresh the data.
  3. The result of the API call is persisted to the DB.
  4. The data is reloaded from the DB when the data change notification is intercepted.

This is very similar to the case presented here, but there is a slight difference.

Since I am using SQLBrite, the DB observables don't terminate (because there is a ContentObserver registered there, that pushes new data down the stream), so methods like concat, merge, etc. won't work.

Currently, I have resolved this using the following approach:

Observable.create(subscriber -> {
    dbObservable.subscribe(subscriber);
    apiObservable
        .subscribeOn(Schedulers.io())
        .observeOn(Schedulers.io())
        .subscribe(
            (data) -> {
                try {
                    persistData(data);
                } catch (Throwable t) {
                    Exceptions.throwOrReport(t, subscriber);
                }
            },

            (throwable) -> {
                Exceptions.throwOrReport(throwable, subscriber);
            })
})

It seems like it's working OK, but it just doesn't seem elegant and "correct".

Can you suggest or point me to a resource that explains what's the best way to handle this situation?

Danail Alexiev
  • 7,624
  • 3
  • 20
  • 28

1 Answers1

6

The solution to your problem is actually super easy and clean if you change the way of thinking a bit. I am using the exact same data interaction (Retrofit + Sqlbrite) and this solution works perfectly.

What you have to do is to use two separate observable subscriptions, that take care of completely different processes.

  1. Database -> View: This one is used to attach your View (Activity, Fragment or whatever displays your data) to the persisted data in db. You subscribe to it ONCE for created View.

dbObservable
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(data -> {
            displayData(data);
        }, throwable -> {
            handleError(throwable);
        });
  1. API -> Database: The other one to fetch the data from api and persist it in the db. You subscribe to it every time you want to refresh your data in the database.

apiObservable
        .subscribeOn(Schedulers.io())
        .observeOn(Schedulers.io())
        .subscribe(data -> {
           storeDataInDatabase(data);
        }, throwable -> {
            handleError(throwable);
        });

EDIT:

You don't want to "transform" both observables into one, purely for the reason you've included in your question. Both observables act completely differently.

The observable from Retrofit acts like a Single. It does what it needs to do, and finishes (with onCompleted).

The observable from Sqlbrite is a typical Observable, it will emit something every time a specific table changes. Theoretically it should finish in the future.

Ofc you can work around that difference, but it would lead you far, far away from having a clean and easily readable code.

If you really, really need to expose a single observable, you can just hide the fact that you're actually subscribing to the observable from retrofit when subscribing to your database.

  1. Wrap the Api subscription in a method:

public void fetchRemoteData() {
    apiObservable
            .subscribeOn(Schedulers.io())
            .observeOn(Schedulers.io())
            .subscribe(data -> {
                persistData(data);
            }, throwable -> {
                handleError(throwable);
            });
}
  1. fetchRemoteData on subscription

dbObservable
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread())
        .doOnSubscribe(() -> fetchRemoteData())
        .subscribe(data -> {
            displayData(data);
        }, throwable -> {
            handleError(throwable);
        });

I suggest you really think about all that. Because the fact that you're forcing yourself into the position where you need a single observable, might be restricting you quite badly. I believe that this will be the exact thing that will force you to change your concept in the future, instead of protecting you from the change itself.

Bartek Lipinski
  • 30,698
  • 10
  • 94
  • 132
  • And how do you abstract this concept to hide the fact that you are using two separate subscriptions? The way I have set it up right now is to have a controller that calls a single method, returning an `Observable`, that does this. Can this be done with the approach you are suggesting? – Danail Alexiev Jan 10 '17 at 15:54
  • The thing is the data has to be refreshed every time it's actually loaded. Another reason is that, in the future, a proper offline mode may have to be developed (based on server - side notifications for data changes, or something like this) and I would like to keep the scope of such a change minimum. Exposing the fact that we have two separate data sources could be a problem in this scenario. – Danail Alexiev Jan 10 '17 at 16:16
  • The solution in the edit looks nice. I have only one last question - what happens to the errors, that may occur in the `fetchRemoteData()` method? If I expose a single observable, it would be nice to be able to redirect all incoming errors to the resulting single subscriber. Do you think this is a good idea or not? – Danail Alexiev Jan 11 '17 at 08:38
  • @DanailAlexiev I don't believe it's a good idea. As I said, you want the subscription to Db to be sort of "permanent". If there is an error in Retrofit stream, and you passed that error to the Db stream you would end up terminating the Db subscription. It would be unable to receive any more updates (emissions). And I believe that's not the desired scenario. If there is an error from api (e.g. you're offline), you don't want your Db subscription to terminate. When your user fixes the connection, and refreshes, the alive Db subscription would handle everything easily (and terminated wouldn't). – Bartek Lipinski Jan 11 '17 at 08:56
  • Thanks for your time and your input – Danail Alexiev Jan 11 '17 at 09:00