I just start learning rxJava for Android and want to implement the common use case:
- request data from cache and show to the user
- request data from web
- server update data in storage and automatically show it to the user
Traditionally on of the best scenarios was use CursorLoader to get data from cache, run web request in the separate thread and save data to the disk via content provider, content provider automatically notify the listener and CursorLoader autoupdate UI.
In rxJava I can do it by running two different Observers as you can see in code below, but I don't find the way how to combine this two calls into the one to reach my aim. Googling shows this thread but it looks like it just get data from the cache or data from the web server, but don't do both RxJava and Cached Data
Code snippet:
@Override
public Observable<SavingsGoals> getCachedSavingsGoal() {
return observableGoal.getSavingsGoals()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
}
@Override
public Observable<SavingsGoals> getRecentSavingsGoal() {
return api.getSavingsGoals()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
}
model.getCachedSavingsGoal().subscribe(new Observer<SavingsGoals>() {
@Override
public void onCompleted() {
// no op
}
@Override
public void onError(Throwable e) {
Log.e(App.TAG, "Failed to consume cached data");
view.showError();
}
@Override
public void onNext(SavingsGoals savingsGoals) {
Log.d(App.TAG, "Show the next item");
if (savingsGoals != null && !savingsGoals.getSavingsGoals().isEmpty()) {
view.showData(savingsGoals.getSavingsGoals());
} else {
view.showError();
}
}
});
model.getRecentSavingsGoal().subscribe(new Observer<SavingsGoals>() {
@Override
public void onCompleted() {
// no op
}
@Override
public void onError(Throwable e) {
Log.e(App.TAG, "Failed to consume data from the web", e);
view.showError();
}
@Override
public void onNext(SavingsGoals savingsGoals) {
if (savingsGoals != null && !savingsGoals.getSavingsGoals().isEmpty()) {
view.showData(savingsGoals.getSavingsGoals());
} else {
view.showError();
}
}
});
Also, the one of issues with current approach is cache and web data are not garranted to be run sequently. It is possible when outdated data will come as latest and override recent from web.
To solve this issue I implemented Observer merge with filtration by timestamp: it get data from cache, pass it to the next observer and if cache is outdated fire new call to the web - case for thread competition solved by the filtration with timestamps. However, the issue with this approach I can not return cache data from this Observable - I need to wait when both requests finish their work.
Code snippet.
@Override
public Observable<Timestamped<SavingsGoals>> getSavingGoals() {
return observableGoal
.getTimestampedSavingsGoals()
.subscribeOn(Schedulers.io())
.flatMap(new Func1<Timestamped<SavingsGoals>, Observable<Timestamped<SavingsGoals>>>() {
@Override
public Observable<Timestamped<SavingsGoals>> call(Timestamped<SavingsGoals> cachedData) {
Log.d(App.FLOW, "getTimestampedSavingsGoals");
return getGoalsFromBothSources()
.filter(filterResponse(cachedData));
}
})
.subscribeOn(AndroidSchedulers.mainThread());
}
private Func1<Timestamped<SavingsGoals>, Boolean> filterResponse(Timestamped<SavingsGoals> cachedData) {
return new Func1<Timestamped<SavingsGoals>, Boolean>() {
@Override
public Boolean call(Timestamped<SavingsGoals> savingsGoals) {
return savingsGoals != null
&& cachedData != null
&& cachedData.getTimestampMillis() < savingsGoals.getTimestampMillis()
&& savingsGoals.getValue().getSavingsGoals().size() != 0;
}
};
}
private Observable<Timestamped<SavingsGoals>> getGoalsFromBothSources() {
Log.d(App.FLOW, "getGoalsFromBothSources:explicit");
return Observable.merge(
observableGoal.getTimestampedSavingsGoals().subscribeOn(Schedulers.io()),
api.getSavingsGoals()
.timestamp()
.flatMap(new Func1<Timestamped<SavingsGoals>, Observable<Timestamped<SavingsGoals>>>() {
@Override
public Observable<Timestamped<SavingsGoals>> call(Timestamped<SavingsGoals> savingsGoals) {
Log.d(App.FLOW, "getGoalsFromBothSources:implicit");
return observableGoal.saveAllWithTimestamp(savingsGoals.getTimestampMillis(), savingsGoals.getValue().getSavingsGoals());
}
}))
.subscribeOn(Schedulers.io());
}
Do you know the approach to do this in one Observer?
Potential solution:
@Override
public Observable<SavingsGoals> getSavingGoals() {
return api.getSavingsGoals()
.publish(network ->
Observable.mergeDelayError(
observableGoal.getSavingsGoals().takeUntil(network),
network.flatMap(new Func1<SavingsGoals, Observable<SavingsGoals>>() {
@Override
public Observable<SavingsGoals> call(SavingsGoals savingsGoals) {
return observableGoal.saveAll(savingsGoals.getSavingsGoals());
}
})
)
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
}
- Sorry, hot replacement in IDE hide the issue which this approach has: first one in case if network unavailable and cache thread completes first, the error will terminate whole merge (solved by mergeDelayError), second one is in case when cache is empty and returns first data from web request will not be return on subscriber. As you can see my method returns Observable after save and traditional merge as I shown in my code properly handle this case but takeUntil by some reason can not. Question is still open.