I have a simple case to resolve, but I haven't found how to implement it with RxJava yet. On one side I have Retrofit (network) and on the other side my SQLite database (cache).
What I would like to achieve is :
If there is nothing in the cache: call the API
If there is something in the cache:
if the dataset is up-to-date: emit only the results from the db
if the dataset is outdated: emit the outdated results and then call the API (and emit the results)
Do you have any idea how to implement this behavior with Rxjava (version 1)?
Thanks a lot !
There's already a lot of articles on that case. To name a few:
Loading data from multiple sources with RxJava - Dan Lew
Pseudo caching : retrieve data first from a cache, then a network
call (using concat, concatEager, merge or publish) - Kaushik
Gopal
Chaining multiple sources with RxJava - Miguel Juárez
Something like this should work:
Observable<Foo> cache = getCacheFoo();
Observable<Foo> network = getNetworkFoo();
Observable<Foo> foos = cache.flatMap(cachedFoo -> {
if (cachedFoo == null) {
// if the cache returns nothing, return the network observable
return network;
} else if (cachedFoo.isStale()) {
// if the cache is stale, return the data, but follow up with the network
return Observable.just(cachedFoo).concatWith(network);
} else {
// otherwise the data is fresh, simply return it
return Observable.just(cachedFoo);
}
});
Related
I am using Room with RxJava2 to implement my data layer via Repository Pattern principles.
I have the following simple code which decides where to pick data from.
#Override
public Single<Team> getTeamById(int teamId) {
return Single.
concat(local.getTeamById(teamId),
remote.getTeamById(teamId)).
filter(team -> team != null).
firstOrError();
}
The problem here is that instead of going to the remote source , it returns an error from the first source (local) if the data was not available.
android.arch.persistence.room.EmptyResultSetException: Query returned empty result set: select * from teams where id = ?
How should I instruct the concat to forgo any error that is received and continue its concatenation?
Aslong you're not sure if you can receive at least one Team from you data provider, you should probably think of using Maybe instead of Single.
You can lookup the definition here:
Single as it states:
it always either emits one value or an error notification
Use Maybe instead:
Maybe
there could be 0 or 1 item or an error signalled by some reactive
source
As your error already states there seems to be a problem while extracting results from your query.
Handle your result extraction correctly, so that you check if there are results before trying extracting any. Therefor the Maybe would either return 0 or 1 item, and not throw any error at all when no Team was found.
You cannot pass null in RxJava2. So whenever your local repo is empty you just can't return null in your single. There was a question o stack about handling null objects: Handle null in RxJava2
Also here you can find an article showing you preferred implementation of repository pattern using RxJava2:
https://android.jlelse.eu/rxjava-2-single-concat-sample-for-repository-pattern-1873c456227a
So simplifying - instead of returning null from both local and remote repo pass some sort of "empty" object. That will be useful also in your business logic allowing you to recognize empty set of data.
If you want to continue when the first source errors (instead of completing as empty), you can use onErrorResumeNext instead of concat (I assume both get calls return Observable, adjust as necessary):
return local.getTeamById(teamId)
.onErrorResumeNext(error -> {
if (error instanceof EmptyResultSetException) {
return remote.getTeamById(teamId));
}
return Observable.error(error);
})
.firstOrError();
I used Maybe to solve my Rxjava2 repository pattern problem.
In your case, I would use the following code to sort it out:
//you may need to rewrite your local.getTeamById method
protected Maybe<Team> getTeamById(int teamId) {
Team team = localDataHelper.getTeamById(teamId);
return team != null ? Maybe.just(team) : Maybe.empty();
}
#Override
public Single<Team> getTeamById(int teamId) {
Maybe<Team> cacheObservable = local.getTeamById(teamId);
Maybe<Team> apiCallObservable = remote.getTeamById(teamId).toMaybe();
return Maybe.concat(cacheObservable, apiCallObservable)
.toSingle();
}
I have the following requirement:
Multiple observers (fragments) need to subscribe to a data source.
Activity will start a network request. Once the request is successful, each observer will receive the result.
I've trying to do it using cache / publish operators, but the issue is when initial request returns an error. At this point I wish to reset the stream and subsequent calls to the method should run a new network request instead of returning an error each time.
Here's what I have currently.
private Flowable<List<Data>> dataObservable;
private Flowable<List<Data>> getData(){
if(dataObservable == null){
dataObservable = apiService.getData()
.doOnError(throwable -> {
dataObservable = null;
})
.cache();
}
return dataObservable;
}
This works, but the code feels wrong. There's got to be a better way.
You can define the observable ahead of time, and it won't actually do anything until something subscribes to it. That's one less null value to worry about.
You can use the retry() operator, or a variant of it, to automatically retry the network operation on an error.
Finally, the cache() operator will ensure that only one network connection subscription is active. Each subscriber will get any updates from the observable, and will be oblivious to any network errors experienced.
Flowable<List<Data>> dataObservable = apiService.getData()
.retry()
.cache();
Apparently there is no operator for that.
The issue was discussed at Observable, retry on error and cache only if completed
and Plato created a nice tiny lib for that
platoblm/rx-onerror-retry-cache.
I know that Dan Lew pretty much answered the question of getting data from different sources with a
.concat(/*...*/)
.take(1)
But what if instead of getting a List of users from both my locally stored data and retrofit.
I needed to do a database specific operation on the data before displaying the result like showing distinct users only. In that case simply using the concat operator on both my network request and local data wouldn't return the same result.
Is there any idiomatic way of writing this with RxJava?
Have you tried distinct()? According documentation, this method will give you only different object when they are emitted. If you have custom objects, i think you must implement equals() and hashCode()
What ended up working really well for me is having the network request return a RealmResult and saving the data just before calling the RealmQuery - so something like:
fun network(): Observable<RealmResult<Something>> {
return getAuth()
.andThen(somethingRepository.getRemoteSomething())
.doOnNext { somethings: List<Something> -> somethingRepository.saveSomethings(somethings) }
.flatMap { distinctSomethings }
}
val distinctSomethings: Observable<RealmResults<Something>> by lazy { getDistinctSomethings() }
//... later
fun showDistinctSomethings() {
Observable.concat(
distinctSomethings,
network()
)
.filter { somethings: RealmResults<Something> ->
somethings.toArray().isNotEmpty()
}
.take(1)
.subscribe(/*..show distinct somethings here.*/)
}
Crucially though, you could replace getDistinctSomethings() with any complex Realm look up and you would always get the right result
Hope this helps someone beside me :P
I am fairly new to rxJava, trying stuff by my own. I would like to get some advice if I'm doing it right.
Usecase: On the first run of my app, after a successful login I have to download and save in a local database several dictionaries for the app to run with. The user has to wait till the downloading process finishes.
Current solution: I am using retrofit 2 with rxjava adapter in order to get the data. I am bundling all Observables into one using the zip operator. After all downloads are done the callback triggers and saving into database begins.
Nothing speaks better than some code:
Observable<List<OrderType>> orderTypesObservable = backendService.getOrderTypes();
Observable<List<OrderStatus>> orderStatusObservable = mockBackendService.getOrderStatuses();
Observable<List<Priority>> prioritiesObservable = backendService.getPriorities();
return Observable.zip(orderTypesObservable,
orderStatusObservable,
prioritiesObservable,
(orderTypes, orderStatuses, priorities) -> {
orderTypeDao.deleteAll();
orderTypeDao.insertInTx(orderTypes);
orderStatusDao.deleteAll();
orderStatusDao.insertInTx(orderStatuses);
priorityDao.deleteAll();
priorityDao.insertInTx(priorities);
return null;
});
Questions:
Should I use the zip operator or is there a better one to fit my cause?
It seems a bit messy doing it this way. This is only a part of the code, I have currently 12 dictionaries to load. Is there a way to refactor it?
I would like to insert a single dictionary data as soon as it finishes downloading and have a retry mechanism it the download fails. How can I achieve that?
I think in your case it's better to use Completable, because for you matter only tasks completion.
Completable getAndStoreOrderTypes = backendService.getOrderTypes()
.doOnNext(types -> *store to db*)
.toCompletable();
Completable getAndStoreOrderStatuses = backendService.getOrderStatuses()
.doOnNext(statuses -> *store to db*)
.toCompletable();
Completable getAndStoreOrderPriorities = backendService.getOrderPriorities()
.doOnNext(priorities -> *store to db*)
.toCompletable();
return Completable.merge(getAndStoreOrderTypes,
getAndStoreOrderStatuses,
getAndStoreOrderPriorities);
If you need serial execution - use Completable.concat() instead of merge()
a retry mechanism if the download fails
Use handy retry() operator
It is not good, to throw null value object into Rx Stream (in zip your return null, it is bad).
Try to not doing that.
In your case, you have 1 api call and 2 actions to save response into the database, so you can create the chain with flatMap.
It will look like:
backendService.getOrderTypes()
.doOnNext(savingToDatabaseLogic)
.flatMap(data -> mockBackendService.getOrderStatuses())
.doOnNext(...)
.flatMap(data -> backendService.getPriorities())
.doOnNext(...)
if you want to react on error situation, in particular, observable, you can add onErrorResumeNext(exception->Observable.empty()) and chain will continue even if something happened
Also, you can create something like BaseDao, which can save any Dao objects.
I am seeking an example of a flow I'm trying to implement with help of RxJava.
Suppose I want to show a list of data. The flow should look something like this:
Read cache. If it contains the data, show it;
Send an API request to the server:
If it returned the data, then cache it and show it.
If it returned and error and there was no cached data, then show an error.
If it returned and error and there was something cached, then do nothing.
Right now I have a method that does something similar (with lots of inspiration from Jake's u2020). The main difference is that it uses in-memory caching, which means there's no need for a separate Observable for reading from cache and it can be done synchronously.
I don't know how to combine two observables (one for reading from cache and the other for API call) and obtain the flow described above.
Any suggestions?
I think I solved my problem. The observable chain looks like so:
apiCall()
.map(data -> dataInMemory = data)
.onErrorResumeNext(t -> data == null ?
Observable.just(Data.empty()) : Observable.empty())
.startWith(readDataFromCache().map(data -> dataInMemory = data))
.subscribeOn(ioScheduler)
.observeOn(uiScheduler)
.subscribe(dataRequest);
The main point is, that if readDataFromCache() throws an error, it will call onCompleted() without calling onError(). So it should be a custom Observable which you can control.
Data.empty() is a stub for my data - the Subscriber should treat it as an error.
dataInMemory is a member in my controller which acts as in-memory cache.
EDIT: the solution doesn't work properly. The completion of one use case (see comment) is not achieved.
EDIT 2: well, the solution does work properly after some tweaking. The fix was returning different types of observables depending on the state of in-memory cache. Kind of dirty.
Here is my solution:
readDataFromCache().defaultIfEmpty(null)
.flatMap(new Func1<Data, Observable<Data>>() {
#Override
public Observable<Data> call(final Data data) {
if (data == null) {
// no cache, show the data from network or throw an error
return apiCall();
} else {
return Observable.concat(
Observable.just(data),
// something cached, show the data from network or do nothing.
apiCall().onErrorResumeNext(Observable.<Data>empty()));
}
}
});
I don't add the subscribeOn and observeOn because I'm not sure readDataFromCache() should use ioScheduler or uiScheduler.