RxJava operator for cached item provider - android

I'm trying to implement a provider that looks for items in memory, disk, network, in this order. The main purpose of this is to avoid network calls if I have the right local cache. There is a catch, since my calls to the network use filters to get items, I could have 10 items from the local query, but still need to go to the network because those items come from a different network call, with different query parameters.
Right now I'm using a concat with a firstOrDefault, checking that the list isn't null or empty. I've implemented a way to check if I already called the server with the specific query and I use it to return null when reading from disk.
I now need to refine the provider, so that it:
emits local items
go online if needed
emits online items
(Right now it stops at the first good list of items).
I'm trying with takeWhile, using a method that returns true if data is null or empty or if I didn't already call the server for that query. The problem is that takeWhile doesn't emit the item if the check for that item is false, meaning I won't get the last good item (which is also the best one).
The best solution I can think of is an operator that emits items until a certain condition comes up and then unsubscribe itself. I can't find one.
EDIT: SOME CODE
Solution 1) using firstOrDefault: will not emit local items if !DiskService.wasDownloaded(), because DiskService return a null List<Item> with !DiskService.wasDownloaded()
public Observable<List<Item>> items() {
List<Observable> obs = new ArrayList<>();
Observable<List<Item>> memoryObs = Observable.defer(this::getMemoryItems);
Observable<List<Item>> diskObs = Observable.defer(this::getDiskItems);
Observable<List<Item>> networkObs = Observable.defer(this::getNetworkItems);
Observable<List<Item>> concat = Observable.concat(memoryObs, diskObs, networkObs;
return concat.firstOrDefault(new ArrayList<>(), this::canAccept);
}
private boolean canAccept(List<Item> data) {
return data != null && data.size() > 0;
}
//Method in DiskService
public boolean wasDownloaded(){
return true if the query was performed on the server, false otherwise.
}
Solution 2) Using takeWhile. The problem with takeWhile is the Observable will not emit the item that doesn't check its condition, meaning I won't get the best List. The hacky solution is to defer the false check to the next item, but this way a network request will be fired even when not necessary. With this solution I'm using a TrustedItemList that just contains the List and a boolean that tells the Observable if he can trust a non-empty list of items (always true for memory and network, true if wasDownloaded() for disk)
public Observable<List<Item>> items() {
List<Observable> obs = new ArrayList<>();
Observable<TrustedItemList> memoryObs = Observable.defer(this::getMemoryItems);
Observable<TrustedItemList> diskObs = Observable.defer(this::getDiskItems);
Observable<TrustedItemList> networkObs = Observable.defer(this::getNetworkItems);
Observable<TrustedItemList> concat = Observable.concat(memoryObs, diskObs, networkObs;
return concat.takeWhile(this::shouldContinueSearching)
.filter(trustedItemList -> trustedItemList.items != null && !trustedItemList.items.isEmpty())
.map(trustedItemList -> trustedItemList.items);
}
private boolean shouldContinueSearching(TrustedPoiList data) {
return data == null || data.items == null || data.items.isEmpty() || !data.canTrustIfNotEmpty;
}

I ended up using a custom Observable.Operator, shamelessly copied from OperatorTakeWhile, with the sole change of calling subscriber.onNext(t) just before subscriber.onCompleted() in the onNext method. This way the last item, the one that returns false on the boolean check, is emitted.
public final class OperatorTakeWhileWithLast<T> implements Observable.Operator<T, T> {
private final Func2<? super T, ? super Integer, Boolean> predicate;
public OperatorTakeWhileWithLast(final Func1<? super T, Boolean> underlying) {
this((input, index) -> {
return underlying.call(input);
});
}
public OperatorTakeWhileWithLast(Func2<? super T, ? super Integer, Boolean> predicate) {
this.predicate = predicate;
}
#Override
public Subscriber<? super T> call(final Subscriber<? super T> subscriber) {
Subscriber<T> s = new Subscriber<T>(subscriber, false) {
private int counter = 0;
private boolean done = false;
#Override
public void onNext(T t) {
boolean isSelected;
try {
isSelected = predicate.call(t, counter++);
} catch (Throwable e) {
done = true;
Exceptions.throwIfFatal(e);
subscriber.onError(OnErrorThrowable.addValueAsLastCause(e, t));
unsubscribe();
return;
}
if (isSelected) {
subscriber.onNext(t);
} else {
done = true;
subscriber.onNext(t); //Just added this line
subscriber.onCompleted();
unsubscribe();
}
}
#Override
public void onCompleted() {
if (!done) {
subscriber.onCompleted();
}
}
#Override
public void onError(Throwable e) {
if (!done) {
subscriber.onError(e);
}
}
};
subscriber.add(s);
return s;
}
}
My items() method (solution 2) now ends with:
return concat.lift(new OperatorTakeWhileWithLast<TrustedItemList>(this::shouldContinueSearching))
.filter(trustedItemList -> trustedItemList.items != null && !trustedItemList.items.isEmpty())
.map(trustedItemList -> trustedItemList.items);

Related

Continue Completable after doOnComplete Android

I am new in RX Java and I have a issue.I have a completable and want to continue my operations after I got a value and did some actions in one of the steps of emmition.
if (mIsCopy) {
completable = AppManagers.getContentManager().completeCopy(mContent).toCompletable().andThen(completable).doOnComplete(() -> {
createThumbnailsForNewContent(mContent); //I want this to be happen before completable.cache();
});
}
completable = completable.cache();
completable.subscribe(new SimpleCompletableSubscriber() {
#Override
public void onComplete() {
TypeUtil.empty(mNewTags);
mOriginalSubmitToUsers.clear();
mOriginalSubmitToUsers.addAll(mSubmitToUsers);
mOriginalSubmitToGroups.clear();
mOriginalSubmitToGroups.addAll(mSubmitToGroups);
mOriginalSubmitToChannels.clear();
mOriginalSubmitToChannels.addAll(mSubmitToChannels);
mOriginalSubmitToRelationships.clear();
mOriginalSubmitToRelationships.addAll(mSubmitToRelationships);
mIsCopy = false;
}
createThumbnailsForNewContent(mContent); returns completable,thx in advance.
public Single<Long> completeCopy(final Content copiedContent) {
return Single.fromCallable(new Callable<Long>() {
#Override
public Long call() throws Exception {
if (!copiedContent.isLocalCopy() || copiedContent.getId() != null) {
throw new IllegalArgumentException("Content passed must be an unsaved local copy.");
}
if (getDBManager().insertOrUpdateContent(copiedContent)) {
if (copiedContent.getCopiedFromCloudID() != null && copiedContent.getCopiedFromCloudID() > 0) {
AppManagers.getAppContext()
.getRequestQueue()
.forRequest(new InitiateCopyRequest(AppUserData.shared.getAccessKey(),
copiedContent.getCopiedFromCloudID()))
.withPriority(RequestQueue.Priority.UPDATE)
.attachLocalID(copiedContent.getId())
.enqueue();
}
} else {
throw new ErrorCodeException(ErrorCodes.UPDATE_CONTENT_FAILED);
}
return copiedContent.getId();
}
}).subscribeOn(getIOScheduler());
}
Don't really get the problem, but according to your code it seems that the method createThumbnailsForNewContent is never executed. That's because doOnComplete accepts as param an Action (this is a Runnable but allows throwing checked exceptions).
If you want to execute createThumbnailsForNewContent once the previous Completable is completed, you have to use the operator andThen(CompletableSource next).

Android: infinite scroll with rx-java using repeatWhen, takeUntil and filter with retrofit

I am using Retrofit 2.2 with RxJava.
The pagination works like this: I get the first batch of data, I have to request the second batch of data with the same params except one which is the lastUpdated date and then if I get empty or the same batch of data it means there are no more items. I have found this great article https://medium.com/#v.danylo/server-polling-and-retrying-failed-operations-with-retrofit-and-rxjava-8bcc7e641a5a#.40aeibaja on how to do it. So my code is:
private Observable<Integer> syncDataPoints(final String baseUrl, final String apiKey,
final long surveyGroupId) {
final List<ApiDataPoint> lastBatch = new ArrayList<>();
Timber.d("start syncDataPoints");
return loadAndSave(baseUrl, apiKey, surveyGroupId, lastBatch)
.repeatWhen(new Func1<Observable<? extends Void>, Observable<?>>() {
#Override
public Observable<?> call(final Observable<? extends Void> observable) {
Timber.d("Calling repeatWhen");
return observable.delay(5, TimeUnit.SECONDS);
}
})
.takeUntil(new Func1<List<ApiDataPoint>, Boolean>() {
#Override
public Boolean call(List<ApiDataPoint> apiDataPoints) {
boolean done = apiDataPoints.isEmpty();
if (done) {
Timber.d("takeUntil : finished");
} else {
Timber.d("takeUntil : will query again");
}
return done;
}
})
.filter(new Func1<List<ApiDataPoint>, Boolean>() {
#Override
public Boolean call(List<ApiDataPoint> apiDataPoints) {
boolean unfiltered = apiDataPoints.isEmpty();
if (unfiltered) {
Timber.d("filtered");
} else {
Timber.d("not filtered");
}
return unfiltered;
}
}).map(new Func1<List<ApiDataPoint>, Integer>() {
#Override
public Integer call(List<ApiDataPoint> apiDataPoints) {
Timber.d("Finished polling server");
return 0;
}
});
}
private Observable<List<ApiDataPoint>> loadAndSave(final String baseUrl, final String apiKey,
final long surveyGroupId, final List<ApiDataPoint> lastBatch) {
return loadNewDataPoints(baseUrl, apiKey, surveyGroupId)
.concatMap(new Func1<ApiLocaleResult, Observable<List<ApiDataPoint>>>() {
#Override
public Observable<List<ApiDataPoint>> call(ApiLocaleResult apiLocaleResult) {
return saveToDataBase(apiLocaleResult, lastBatch);
}
});
}
private Observable<ApiLocaleResult> loadNewDataPoints(final String baseUrl, final String apiKey,
final long surveyGroupId) {
Timber.d("loadNewDataPoints");
return Observable.just(true).concatMap(new Func1<Object, Observable<ApiLocaleResult>>() {
#Override
public Observable<ApiLocaleResult> call(Object o) {
Timber.d("loadNewDataPoints call");
return restApi
.loadNewDataPoints(baseUrl, apiKey, surveyGroupId,
getSyncedTime(surveyGroupId));
}
});
}
As you can see the interesting method is loadNewDataPoints and I want it to be called until there are no more datapoints. As you can see Observable.just(true).concatMap is a hack because if I remove this concat map the restApi.loadNewDataPoints(....) does not get called although in the logs I can see that the api does get called but with the same old params and of course it returns the same results as the first time so syncing stops, saveToDataBase does get called fine. With my hack it works but I want to understand why it does not work the other way and also if there is a better way to do this. Thanks a lot!
So, I've written this kind of APIs (it's called Keyset Pagination) and implemented Rx clients against them.
This is one of the cases where BehaviorSubjects are useful:
S initialState = null;
BehaviorProcessor<T> subject = BehaviorProcessor.createDefault(initialState);
return subject
.flatMap(state -> getNextElements(state).singleOrError().toFlowable(), Pair::of, 1)
.serialize()
.flatMap(stateValuePair -> {
S state = stateValuePair.getLeft();
R retrievedValue = stateValuePair.getRight();
if(isEmpty(retrievedValue)) {
subject.onComplete();
return Flowable.empty();
} else {
subject.onNext(getNextState(state, retrievedValue));
return Flowable.just(retrievedValue);
}
}
.doOnUnsubscribe(subject::onCompleted)
.map(value -> ...)
Here
getNextElement performs the network call based on a state and returns a reactive stream with a single value
isEmpty determines whether the returned value is empty indicating end of elements
getNextState combines the passed-in state with the retrieved value to determine the next state for getNextElement.
It will work correctly if an error occurs (it will be propagated) and if you unsubscribe before the end (queries will get terminated).
Of course, in your specific case these don't need to be separate methods or complex types.

Long running RxJava Subscriptions with refreshable data

I'm looking to set up a long running data subscription to a particular data object in Android/RxJava. Specifically a combination of a Retrofit REST call paired with cached data. I've done this pretty simply just wrapping an API call with data, were the API call is Retrofit returning an Observable:
class OpenWeather {
...
Observable<CurrentWeather> OpenWeather.getLocalWeather()
...
}
The simple implementation would be:
public static Observable<CurrentWeather> getWeatherOnce() {
if (currentWeather != null)
return Observable.just(currentWeather);
return OpenWeather.getLocalWeather()
.map(weather -> currentWeather = weather);
}
private static CurrentWeather currentWeather;
The problem is that there is no way to notify when the "current weather" has been updated. The simplest way to add refreshable data with long running updates between subscriptions would be to use a BehaviorSubject like such:
public class DataModel {
public enum DataState {
ANY, // whatever is available, don't require absolute newest
LATEST, // needs to be the latest and anything new
}
private final static BehaviorSubject<CurrentWeather> currentWeatherSubject = BehaviorSubject.create();
public static Observable<CurrentWeather> getCurrentWeather(DataState state) {
synchronized (currentWeatherSubject) {
if (state == DataState.LATEST || currentWeatherSubject.getValue() == null) {
OpenWeather.getLocalWeather()
.subscribeOn(Schedulers.io())
.toSingle()
.subscribe(new SingleSubscriber<CurrentWeather>() {
#Override
public void onSuccess(CurrentWeather currentWeather) {
currentWeatherSubject.onNext(currentWeather);
}
#Override
public void onError(Throwable error) {
// ?? currentWeatherSubject.onError(error);
}
});
}
}
return currentWeatherSubject.asObservable();
}
}
Using the BehaviorSubject, when getting the current weather, get either the last cached entry and any updates as they occur. Thoughts?
So I'm sure I'm doing something wrong here as there seems there should be an easier way or more elegant way.

RxJava: database and remote server

I have a list of objects that I want retrieve from a local database (if available), or from a remote server otherwise. I'm using RxJava Observables (SqlBrite for the database and Retrofit for the remote server).
My query code is as follows:
Observable<List<MyObject>> dbObservable = mDatabase
.createQuery(MyObject.TABLE_NAME,MyObject.SELECT_TYPE_A)
.mapToList(MyObject.LOCAL_MAPPER);
Observable<List<MyObject>> remoteObservable = mRetrofitService.getMyObjectApiService().getMyObjects();
return Observable.concat(dbObservable, remoteObservable)
.first(new Func1<List<MyObject>, Boolean>() {
#Override
public Boolean call(List<MyObject> myObjects) {
return !myObjects.isEmpty();
}
});
I see the first observable running and hitting the first method with an empty list, but then the retrofit observable does not run, there is no network request. If I switch the order of the observables, or just return the remote observable, it works as expected, it hits the remote server and returns the list of objects.
Why would the remote observable fail to run in this scenario? The subscriber's onNext, orError and onComplete methods are not called when I concatenate the observables with the db first and retrofit second.
Thanks!
Kaushik Gopal has addressed this in his RxJava-Android-Samples github project.
He recommends using this technique:
getFreshNetworkData()
.publish(network ->
Observable.merge(network,
getCachedDiskData().takeUntil(network)))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Subscriber<List<MyObject>() {
...
});
In your case, it might look like this:
remoteObservable
.publish(network ->
Observable.merge(network,
dbObservable.takeUntil(network)))
.first(myObjects -> !myObjects.isEmpty());
Edit: It sounds like you just might need this:
dbObservable
.flatMap(localResult -> {
if (localResult.isEmpty()) {
return remoteObservable;
} else {
return Observable.just(localResult);
}
});
I assume you have your observables which can get data from your local and remote like below:
final Observable<Page> localResult = mSearchLocalDataSource.search(query);
final Observable<Page> remoteResult = mSearchRemoteDataSource.search(query)
.doOnNext(new Action1<Page>() {
#Override
public void call(Page page) {
if (page != null) {
mSearchLocalDataSource.save(query, page);
mResultCache.put(query, page);
}
}
});
Then you can map them and get first which means if local available use local if not use remote:
return Observable.concat(localResult, remoteResult)
.first()
.map(new Func1<Page, Page>() {
#Override
public Page call(Page page) {
if (page == null) {
throw new NoSuchElementException("No result found!");
}
return page;
}
});
And subscribe it like below:
mCompositeSubscription.clear();
final Subscription subscription = mSearchRepository.search(this.mQuery)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Observer<Page>() {
#Override
public void onCompleted() {
// Completed
}
#Override
public void onError(Throwable e) {
mView.onDefaultMessage(e.getMessage());
}
#Override
public void onNext(Page page) {
mView.onDefaultMessage(page.getContent());
}
});
mCompositeSubscription.add(subscription);
For more detail or example you can check my github repo:
https://github.com/savepopulation/wikilight
Good luck!
Edit:
You can try a local observable like below. Simply it checks if there's a record and returns an empty observable.
#Override
public Observable<Page> search(#NonNull final String query) {
return Observable.create(new Observable.OnSubscribe<Page>() {
#Override
public void call(Subscriber<? super Page> subscriber) {
final Realm realm = Realm.getInstance(mRealmConfiguration);
final Page page = realm.where(Page.class)
.equalTo("query", query)
.findFirst();
if (page != null && page.isLoaded() && page.isValid()) {
Log.i("data from", "realm");
subscriber.onNext(realm.copyFromRealm(page));
} else {
Observable.empty();
}
subscriber.onCompleted();
realm.close();
}
});
}
Edit 2:
When you return null from local concat and first will not work and your remote will not be called because null means observable returns null but still can observe. When you return observable.empty with concat and first this means observable cannot emit anything from local more and so it can emit from remote.

Handling API exceptions in RxJava

I'm trying to wrap my head around RxJava currently, but I'm having a little trouble with handling service call exceptions in an elegant manner.
Basically, I have a (Retrofit) service that returns an Observable<ServiceResponse>. ServiceResponse is defined like so:
public class ServiceResponse {
private int status;
private String message;
private JsonElement data;
public JsonElement getData() {
return data;
}
public int getStatus() {
return status;
}
public String getMessage() {
return message;
}
}
Now what I want is to map that generic response to a List<Account> contained within the data JsonElement field (I assume you don't care what the Account object looks like, so I won't pollute the post with it). The following code works really well for the success case, but I can't find a nice way to handle my API exceptions:
service.getAccounts()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.map(new Func1<ServiceResponse, AccountData>() {
#Override
public AccountData call(ServiceResponse serviceResponse) {
// TODO: ick. fix this. there must be a better way...
ResponseTypes responseType = ResponseTypes.from(serviceResponse.getStatus());
switch (responseType) {
case SUCCESS:
Gson gson = new GsonBuilder().create();
return gson.fromJson(serviceResponse.getData(), AccountData.class);
case HOST_UNAVAILABLE:
throw new HostUnavailableException(serviceResponse.getMessage());
case SUSPENDED_USER:
throw new SuspendedUserException(serviceResponse.getMessage());
case SYSTEM_ERROR:
case UNKNOWN:
default:
throw new SystemErrorException(serviceResponse.getMessage());
}
}
})
.map(new Func1<AccountData, List<Account>>() {
#Override
public List<Account> call(AccountData accountData) {
Gson gson = new GsonBuilder().create();
List<Account> res = new ArrayList<Account>();
for (JsonElement account : accountData.getAccounts()) {
res.add(gson.fromJson(account, Account.class));
}
return res;
}
})
.subscribe(accountsRequest);
Is there a better way to do this? This does work, onError will fire to my observer, and I will receive the error that I threw, but it definitely does not seem like I'm doing this right.
Thanks in advance!
Edit:
Let me clarify exactly what I want to achieve:
I want to have a class that can be called from the UI (e.g. an Activity, or Fragment, or whatever). That class would take an Observer<List<Account>> as a parameter like so:
public Subscription loadAccounts(Observer<List<Account>> observer, boolean forceRefresh) {
...
}
That method would return a subscription that can be unsubscribed when the UI is detached/destroyed/etc.
The parameterized observer would handle onNext for the successful responses passing in a list of Accounts. OnError would handle any exceptions, but would also get passed any API exceptions (e.g. if the response status != 200 we would create a Throwable and pass it to onError). Ideally I don't want to just "throw" the Exception, I want to pass it directly to the Observer. That's what all the examples I see do.
The complication is that my Retrofit service returns a ServiceResponse object, so my observer cannot subscribe to that. The best I've come up with is to create an Observer wrapper around my Observer, like so:
#Singleton
public class AccountsDatabase {
private AccountsService service;
private List<Account> accountsCache = null;
private PublishSubject<ServiceResponse> accountsRequest = null;
#Inject
public AccountsDatabase(AccountsService service) {
this.service = service;
}
public Subscription loadAccounts(Observer<List<Account>> observer, boolean forceRefresh) {
ObserverWrapper observerWrapper = new ObserverWrapper(observer);
if (accountsCache != null) {
// We have a cached value. Emit it immediately.
observer.onNext(accountsCache);
}
if (accountsRequest != null) {
// There's an in-flight network request for this section already. Join it.
return accountsRequest.subscribe(observerWrapper);
}
if (accountsCache != null && !forceRefresh) {
// We had a cached value and don't want to force a refresh on the data. Just
// return an empty subscription
observer.onCompleted();
return Subscriptions.empty();
}
accountsRequest = PublishSubject.create();
accountsRequest.subscribe(new ObserverWrapper(new EndObserver<List<Account>>() {
#Override
public void onNext(List<Account> accounts) {
accountsCache = accounts;
}
#Override
public void onEnd() {
accountsRequest = null;
}
}));
Subscription subscription = accountsRequest.subscribe(observerWrapper);
service.getAccounts()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(accountsRequest);
return subscription;
}
static class ObserverWrapper implements Observer<ServiceResponse> {
private Observer<List<Account>> observer;
public ObserverWrapper(Observer<List<Account>> observer) {
this.observer = observer;
}
#Override
public void onCompleted() {
observer.onCompleted();
}
#Override
public void onError(Throwable e) {
observer.onError(e);
}
#Override
public void onNext(ServiceResponse serviceResponse) {
ResponseTypes responseType = ResponseTypes.from(serviceResponse.getStatus());
switch (responseType) {
case SUCCESS:
Gson gson = new GsonBuilder().create();
AccountData accountData = gson.fromJson(serviceResponse.getData(), AccountData.class);
List<Account> res = new ArrayList<>();
for (JsonElement account : accountData.getAccounts()) {
res.add(gson.fromJson(account, Account.class));
}
observer.onNext(res);
observer.onCompleted();
break;
default:
observer.onError(new ApiException(serviceResponse.getMessage(), responseType));
break;
}
}
}
}
I still feel like I am not using this correctly though. I definitely haven't seen anyone else using an ObserverWrapper before. Perhaps I shouldn't be using RxJava, though the guys at SoundCloud and Netflix really sold me on it in their presentations and I'm pretty eager to learn it.
Please read below I've added an edit.
It's perfectly correct to throw within an Action/Func/Observer with RxJava. The exception will be propagate by the framework right down to your Observer.
If you limit yourself to calling onError only then you'll be twisting yourself to make that happen.
With that being said a suggestion would be to simply remove this wrapper and add a simple validation
Action within the service.getAccount... chain of Observables.
I'd use the doOnNext(new ValidateServiceResponseOrThrow) chained with a map(new MapValidResponseToAccountList). Those are simple classes which implements the necessary code to keep the Observable chain a bit more readable.
Here's your loadAccount method simplified using what I suggested.
public Subscription loadAccounts(Observer<List<Account>> observer, boolean forceRefresh) {
if (accountsCache != null) {
// We have a cached value. Emit it immediately.
observer.onNext(accountsCache);
}
if (accountsRequest != null) {
// There's an in-flight network request for this section already. Join it.
return accountsRequest.subscribe(observer);
}
if (accountsCache != null && !forceRefresh) {
// We had a cached value and don't want to force a refresh on the data. Just
// return an empty subscription
observer.onCompleted();
return Subscriptions.empty();
}
accountsRequest = PublishSubject.create();
accountsRequest.subscribe(new EndObserver<List<Account>>() {
#Override
public void onNext(List<Account> accounts) {
accountsCache = accounts;
}
#Override
public void onEnd() {
accountsRequest = null;
}
});
Subscription subscription = accountsRequest.subscribe(observer);
service.getAccounts()
.doOnNext(new ValidateServiceResponseOrThrow())
.map(new MapValidResponseToAccountList())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(accountsRequest);
return subscription;
}
private static class ValidateResponseOrThrow implements Action1<ServiceResponse> {
#Override
public void call(ServiceResponse response) {
ResponseTypes responseType = ResponseTypes.from(serviceResponse.getStatus());
if (responseType != SUCCESS)
throw new ApiException(serviceResponse.getMessage(), responseType));
}
}
private static class MapValidResponseToAccountList implements Func1<ServiceResponse, List<Account>> {
#Override
public Message call(ServiceResponse response) {
// add code here to map the ServiceResponse into the List<Accounts> as you've provided already
}
}
Edit:
Unless someone says otherwise I think it's best practice to return errors using flatMap.
I've thrown Exceptions from Action in the past but I don't believe it's the recommended way.
You'll have a cleaner Exception stack if you use flatMap. If you throw from inside an Action the Exception stack
will actually contain rx.exceptions.OnErrorThrowable$OnNextValue Exception which isn't ideal.
Let me demonstrate the example above using the flatMap instead.
private static class ValidateServiceResponse implements rx.functions.Func1<ServiceResponse, Observable<ServiceResponse>> {
#Override
public Observable<ServiceResponse> call(ServiceResponse response) {
ResponseTypes responseType = ResponseTypes.from(serviceResponse.getStatus());
if (responseType != SUCCESS)
return Observable.error(new ApiException(serviceResponse.getMessage(), responseType));
return Observable.just(response);
}
}
service.getAccounts()
.flatMap(new ValidateServiceResponse())
.map(new MapValidResponseToAccountList())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(accountsRequest);
As you can see the the difference is subtle. The ValidateServiceResponse now implements the Func1 instead of Action1 and we're no longer using the throw keyword. We use Observable.error(new Throwable) instead. I believe this fits better with the expected Rx contract.
You could read this good article about error handling http://blog.danlew.net/2015/12/08/error-handling-in-rxjava/

Categories

Resources