How to handel no results with Android Room and RxJava 2? - android

I have database with table contact and I want to check if there is contact with some phone number.
#Query("SELECT * FROM contact WHERE phone_number = :number")
Flowable<Contact> findByPhoneNumber(int number);
I have RxJava 2 Composite disposable with statement from above to check if there is contact with phone number.
disposable.add(Db.with(context).getContactsDao().findByPhoneNumber(phoneNumber)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeWith(new DisposableSubscriber<Contact>() {
#Override
public void onNext(final Contact contact) {
Log.d("TAG", "phone number fined");
Conversation conversation;
if(contact != null){
conversation = Db.with(context).getConversationsDao().findBySender(contact.getContactId());
if(conversation != null){
conversation.setUpdatedAt(Utils.getDateAndTimeNow());
saveConversation(contact, conversation, context, text, phoneNumber, false);
} else {
conversation = getConversation(contact, contact.getPhoneNumber());
saveConversation(contact, conversation, context, text, phoneNumber, true);
}
} else {
conversation = Db.with(context).getConversationsDao().findByPhone(phoneNumber);
if(conversation != null){
conversation.setUpdatedAt(Utils.getDateAndTimeNow());
saveConversation(contact, conversation, context, text, phoneNumber, false);
} else {
conversation = getConversation(contact, phoneNumber);
saveConversation(contact, conversation, context, text, phoneNumber, true);
}
}
}
#Override
public void onError(Throwable t) {
Log.d("TAG", "find phone number throwable");
Toast.makeText(context, t.getLocalizedMessage(), Toast.LENGTH_LONG).show();
}
#Override
public void onComplete() {
Log.d("TAG", "onComplete");
}
}));
This is working fine if query can find contact with required phone number, but if there is result, it nothing happens.
Here are two test cases that I wrote and they work fine:
#RunWith(AndroidJUnit4.class)
public class ContactsTest {
private AppDatabase db;
#Rule
public InstantTaskExecutorRule instantTaskExecutorRule =
new InstantTaskExecutorRule();
#Before
public void initDb() throws Exception {
db = Room.inMemoryDatabaseBuilder(
InstrumentationRegistry.getContext(),
AppDatabase.class)
// allowing main thread queries, just for testing
.allowMainThreadQueries()
.build();
}
#After
public void close(){
db.close();
}
#Test
public void insertAndFindTest(){
final Contact contact = new Contact();
contact.setName("Test");
contact.setPhoneNumber(555);
db.contactsDao()
.insert(contact);
db.contactsDao().findByPhoneNumber(contact.getPhoneNumber())
.test()
.assertValue(new Predicate<Contact>() {
#Override
public boolean test(#NonNull Contact savedContact) throws Exception {
if(savedContact.getPhoneNumber() == contact.getPhoneNumber()){
return true;
}
return false;
}
});
}
#Test
public void findNoValues(){
db.contactsDao().findByPhoneNumber(333)
.test()
.assertNoValues();
}
}
How I can solve this?

As said here, you can use Maybe or Single for this case:
Maybe
#Query("SELECT * FROM Users WHERE id = :userId")
Maybe<User> getUserById(String userId);
Here’s what happens:
When there is no user in the database and the query returns no rows, Maybe will complete.
When there is a user in the database, Maybe will trigger onSuccess and it will complete.
If the user is updated after Maybe was completed, nothing happens.
Single
#Query("SELECT * FROM Users WHERE id = :userId")
Single<User> getUserById(String userId);
Here are some scenarios:
When there is no user in the database and the query returns no rows, Single will trigger onError(EmptyResultSetException.class)
When there is a user in the database, Single will trigger onSuccess.
If the user is updated after Single.onComplete was called, nothing happens, since the stream was completed.
It was added in version 1.0.0-alpha5.

If you want to use your entity only once, Single or Maybe is sufficient. But if you want to observe if your query is updated you can use Flowable and wrap your object in List, so when there is no results you will get empty list, and when after that, database is updated you will get another event with your result in list.
Code
#Query("SELECT * FROM contact WHERE phone_number = :number LIMIT 1")
Flowable<List<Contact>> findByPhoneNumber(int number)
I believe it's usefull in some scenarios. The drawback is that you have to access object like resultList.get(0)

When you use Flowable (and LiveData too) as a return value in your Dao class then your query never stops emitting data as Room is monitoring tables for data changes. Quoting official documentation:
Furthermore, if the response is an observable data type, such as
Flowable or LiveData, Room watches all tables referenced in the query
for invalidation.
Not sure what's the best way of handling such situation but what worked for me was a good old .timeout() operator. Please have a look at the following test and follow comments:
#Test
public void shouldCompleteIfForced() throws InterruptedException {
// given
TestScheduler testScheduler = new TestScheduler();
// when asking db for non existent project
TestSubscriber<Project> test = projectDao.getProject("non existent project")
.timeout(4, TimeUnit.SECONDS, testScheduler)
.test();
// then hang forever waiting for first emission which will never happen
// as there is no such project
test.assertNoValues();
test.assertNotComplete();
test.assertNoErrors();
// when time passes and we trigger timeout() operator
testScheduler.advanceTimeBy(10, TimeUnit.SECONDS);
// then finally break stream with TimeoutException error ...
test.assertError(TimeoutException.class);
}

I guess you also could use wrapper with Single. Like:
public class QueryResult<D> {
public D data;
public QueryResult() {}
public QueryResult(D data) {
this.data = data;
}
public boolean isEmpty(){
return data != null;
}
}
And use it like:
public Single<QueryResult<Transaction>> getTransaction(long id) {
return createSingle(() -> database.getTransactionDao().getTransaction(id))
.map(QueryResult::new);
}
Where createAsyncSingle:
protected <T> Single<T> createSingle(final Callable<T> func) {
return Single.create(emitter -> {
try {
T result = func.call();
emitter.onSuccess(result);
} catch (Exception ex) {
Log.e("TAG", "Error of operation with db");
}
});
}
Don't forget to use IO thread.

#Query("SELECT * FROM contact WHERE phone_number = :number")
Flowable<List<Contact>> findByPhoneNumber(int number);
then
Optional<Contact> queryPhone(int number) {
findByPhoneNumber(number).map { list ->
if (list.isEmpt()) return Optional.empty() else return Optional.of(list[0])
}
}
as described here
I find it's strange that this behaviour is no where to be found on official docs though

Related

Executing rx.Obseravables secuentially

I'm developing an Android App using Fernando Ceja's clean architecture. One of my Interactors or Use Cases is in charge of getting the User's feed data. In order to get the data, first I have to retrieve the User's Teams from a database table and then I have to get the Feed list from the server-side.
This is how I get the Teams from the database layer:
mTeamCache.getAllTeams().subscribe(new DefaultSubscriber<List<SimpleTeam>>() {
#Override
public void onNext(List<SimpleTeam> simpleTeams) {
super.onNext(simpleTeams);
mTeams = simpleTeams;
}
});
TeamCache is basically just another Interactor that takes care of getting all the teams that I have in the database.
Here's how I get the Feed data from the server-side:
mFeedRepository.getFeed(0, 50).subscribe(new ServerSubscriber<List<ApiFeedResponse>>() {
#Override
protected void onServerSideError(Throwable errorResponse) {
callback.onFeedFetchFailed(...);
}
#Override
protected void onSuccess(List<ApiFeedResponse> responseBody) {
//Do stuff with mTeams
callback.onFeedFetched(...);
}
});
My GetFeedInteractor class has a method called execute, where I pass through the Callback that I'm later using in the UI to handle the response. The issue with all this is that currently I'm chaining the responses like this:
#Override
public void execute(final Callback callback, String userSipId) {
mTeamCache.getAllTeams().subscribe(new DefaultSubscriber<List<SimpleTeam>>() {
#Override
public void onNext(List<SimpleTeam> simpleTeams) {
super.onNext(simpleTeams);
mTeams = simpleTeams;
getFeedFromRepository(callback);
}
});
}
public void getFeedFromRepository(final Callback callback) {
mFeedRepository.getFeedRx(0, 50).subscribe(new ServerSubscriber<List<ApiFeedResponse>>() {
#Override
protected void onServerSideError(Throwable errorResponse) {
callback.onFeedFetchFailed("failed");
}
#Override
protected void onSuccess(List<ApiFeedResponse> responseBody) {
//Do stuff with mTeams
List<BaseFeedItem> responseList = new ArrayList();
for (ApiFeedResponse apiFeedResponse : responseBody) {
responseList.add(FeedDataMapper.transform(apiFeedResponse));
}
callback.onFeedFetched(responseList);
}
});
}
As you can see, once that I get the Team collection from the Cache Interactor I call the method that gets the feed from the very same Subscriber. I don't like this. I want to be able to do something nicer, like using Observable.concat(getTeamsFromCache(), getFeedFromRepository()); chain a call to another rx.Observable inside a Subscriber is not something nice to do. I guess that my question is, how can I chain two rx.Observables that are using different Subscribers?
Update:
ServerSubscriber is a subscriber that I implemted to subscribe to Retrofit services. It simply checks the error codes and some stuff. Here is:
https://gist.github.com/4gus71n/65dc94de4ca01fb221a079b68c0570b5
Default subscriber is an empty default subscriber. Here is:
https://gist.github.com/4gus71n/df501928fc5d24c2c6ed7740a6520330
TeamCache#getAllTeams() returns rx.Observable>
FeedRepository#getFeed(int page, int offset) returns rx.Observable>
Update 2:
This is how the Interactor to get the User's feed looks like now:
#Override
public void execute(final Callback callback, int offset, int pageSize) {
User user = mGetLoggedUser.get();
String userSipid = mUserSipid.get();
mFeedRepository.getFeed(offset, pageSize) //Get items from the server-side
.onErrorResumeNext(mFeedCache.getFeed(userSipid)) //If something goes wrong take it from cache
.mergeWith(mPendingPostCache.getAllPendingPostsAsFeedItems(user)) //Merge the response with the pending posts
.subscribe(new DefaultSubscriber<List<BaseFeedItem>>() {
#Override
public void onNext(List<BaseFeedItem> baseFeedItems) {
callback.onFeedFetched(baseFeedItems);
}
#Override
public void onError(Throwable e) {
if (e instanceof ServerSideException) {
//Handle the http error
} else if (e instanceof DBException) {
//Handle the database cache error
} else {
//Handle generic error
}
}
});
}
I think you're missing the point of RxJava and reactive approach, you should not have different subscribers with OO hierarchy, and callbacks.
You should construct separated Observables that should emit the specific data it's handle, without the Subscriber, then you can chain you're Observable as needed, and at the end, you have the subscriber that react to the final result expected from the chained Observable stream.
something like this (using lambdas to have more thin code):
TeamCache mTeamCache = new TeamCache();
FeedRepository mFeedRepository = new FeedRepository();
Observable.zip(teamsObservable, feedObservable, Pair::new)
.subscribe(resultPair -> {
//Do stuff with mTeams
List<BaseFeedItem> responseList = new ArrayList();
for (ApiFeedResponse apiFeedResponse : resultPair.second) {
responseList.add(FeedDataMapper.transform(apiFeedResponse));
}
}, throwable -> {
//handle errors
}
);
I've use zip and not concat as it's seems you have 2 independent calls here that you want to wait for both to finish ('zip' them together) and then act upon, but ofcourse, as you have separated Observables stream, you can chain them together differently according to your needs.
as for your ServerSubscriber with all the response validation logic, it should be rxify too, so you can compose it along your server Observable stream.
something like this (some logic emitted to simplify, and as I'm not familiar with it...)
Observable<List<SimpleTeam>> teamsObservable = mTeamCache.getAllTeams();
Observable<List<ApiFeedResponse>> feedObservable = mFeedRepository.getFeed(0, 50)
.flatMap(apiFeedsResponse -> {
if (apiFeedsResponse.code() != 200) {
if (apiFeedsResponse.code() == 304) {
List<ApiFeedResponse> body = apiFeedsResponse.body();
return Observable.just(body);
//onNotModified(o.body());
} else {
return Observable.error(new ServerSideErrorException(apiFeedsResponse));
}
} else {
//onServerSideResponse(o.body());
return Observable.just(apiFeedsResponse.body());
}
});

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.

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.

RxAnadroid concatenating streams

I'm making a simple weather app to learn RxAndroid and I'm faced with the following issue.
I first load cities I'm interested in and then ask for the weather of each one of them.
getCitiesUseCase returns an Observable<List<City>> that I load from the data base. I send that list of cities to my view to display them and then ask for the weather individually (flatmap) inside the subscriber.
Subscription subscription = getCitiesUseCase.execute().flatMap(new Func1<List<City>, Observable<City>>() {
#Override
public Observable<City> call(List<City> cities) {
citiesView.addCities(cities);
return Observable.from(cities);
}
}).subscribe(new Subscriber<City>() {
#Override
public void onCompleted() {
subscriptions.remove(this);
this.unsubscribe();
}
#Override
public void onError(Throwable e) {
Log.e(this.getClass().getSimpleName(), e.toString());
}
#Override
public void onNext(City city) {
getCityWeatherUseCase.setLatLon(city.getLat().toString(), city.getLon().toString(), city.getId());
getCityWeather(city);
}
});
subscriptions.add(subscription);
Now the getCityWeather() method looks like this:
private void getCityWeather(final City city) {
subscriptions.add(getCityWeatherUseCase.execute().subscribe(new Subscriber<CityWeather>() {
#Override
public void onCompleted() {
}
#Override
public void onError(Throwable e) {
Log.e("error", e.toString());
}
#Override
public void onNext(CityWeather cityWeather) {
city.setCityWeather(cityWeather);
citiesView.updateCity(city);
}
}));
}
Everything works fine and as expected, but the fact that I'm subscribing to an observer inside a subcriber doesnt feel right. I know rxJava lets you play around with subscribers to prevent this kind of things but I really dont know how to improve my code further. Keep in mind that I need a city in order to ask for its weather.
Merry chrismas!
One approach could be the following. (I'm using retrolambda - so wherever you see ->, just replace with a new anonymous inner class).
Note that I'm using flatMap to spin up the weather data requests, rather than Observable.concat like your question suggests. The reason for this is that your scheduler (e.g. io()) will handle these in parallel and send the results through when they are available. However, with Observable.concat, these requests would be serialized so they'd be forced to happen one at a time - nullifying the benefits of a thread pool like io().
private class City {
public String name;
public City(String name) {
this.name = name;
}
public void setWeather(Weather weather) { /*...*/ }
}
private class Weather {
public String status;
public Weather(String status) {
this.status = status;
}
}
private Observable<Weather> getWeather(City city) {
// call your weather API here..
return Observable.just(new Weather("Sunny"));
}
#Test
public void test() {
Observable<List<City>> citiesObs = Observable.create(new Observable.OnSubscribe<List<City>>() {
#Override
public void call(Subscriber<? super List<City>> subscriber) {
// do work
final List<City> cities = new ArrayList<>();
cities.add(new City("Paris"));
cities.add(new City("Tokyo"));
cities.add(new City("Oslo"));
// send results
if (!subscriber.isUnsubscribed()) {
subscriber.onNext(cities);
subscriber.onCompleted();
}
}
});
Observable<City> obs = citiesObs
// inject a side effect
.doOnNext(list -> {
// pass `list` to your view here
})
// turn Observable<Iterable<T>> into Observable<T>
.flatMapIterable(list -> list)
// Map a city to an observable that fetches Weather data
// Your scheduler can take care of these at once.
.flatMap(city -> {
return getWeather(city)
// another side effect
.doOnNext(weather -> {
city.setWeather(weather);
})
// map baack to city, just for the heck of it
.map($ -> city);
});
TestSubscriber sub = TestSubscriber.create();
obs.subscribe(sub);
sub.awaitTerminalEvent();
sub.assertValueCount(3);
}
Also note that in order to take advantage of io(), you'd need to add a call to subscribeOn(Schedulers.io()) to tell the observable to begin doing work on the io thread pool. When you want to pass control to another thread, for example your view, you could insert a observeOn(AndroidSchedulers.mainThread()) before your side-effect (or mapping). If you want to bounce control back to the background thread(s) for your weather calls, you could then add another call to observeOn(Schedulers.io()) right before you flatMap to getWeather(City).

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