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();
}
Related
I must be doing something wrong with Kotlin implementation of view models
I have a view model that has a function to retrieve youtube video id from url.
fun getYoutubeVideoId(url: String): String?{
return "([a-zA-Z0-9_-]{11})".toRegex().find(url)?.value
}
I feel like I'm always in catch 22 because I use this function in a fragment inside with LiveData observable, which forces me to to ? on objects, which then forces me to have return type with ?, which then tirggers if statements to check if objects aren't null.
Here is the vm var
val streamUrl= mainState.getOrNull { it?.account?.streamUrl ?: 0}.distinctUntilChanged()
Here is my shortened observable
streamUrl.observe{
playVideo(getYoutubeVideoId(it))
}
The error from above statement is that it
Requires a String and I'm passing Any
Return should be String and its String?
I'm running around to make sure the types match and its always something not matching or being right. I think I could setup another streamUrl variable under the viewModel besides the observable, but I feel like I should be able to just do it of a single variable.
I hope this makes sense.
So the first thing to embrace with kotlin is: Null Safety.
Null Safety does not mean that you do not get nulls.
It means, that if something is possibly null, the compiler forces you to think about it and handle it at a point that makes sense. If you don't, you potentially get the notorious NullPointerException at an unexpected and possibly ugly point of execution.
So, to eliminate the ? think about where you want to handle the possibility of it being null -> check it -> handle it in an elegant way, and then safely pass the checked result without a ? to the rest of your code.
In order to build my repository, I am retrieving 2 streams of data from Local Database and from remote API. I am trying to access the remote stream only if the mQuakesLocalDataSource has no items by using the concat operator as shown in many examples.
#NonNull
#Override
public Single<List<Quake>> getQuakes(){
return Single.concat(mQuakesLocalDataSource.getQuakes(),
mQuakesRemoteDataSource.getQuakes())
.first(dummyList);
}
The issue I am facing is that mQuakesRemoteDataSource never returns a stream while mQuakesLocalDataSource is empty, therefore I am not having any data result. I have tested mQuakesRemoteDataSource in isolation and without the concat operator it seems to be retrieving its appropriate stream.
Why is this happening?
mQuakesLocalDataSource is based on Room and so therefore it should emit its stream and then complete, so it's not possible that the local source would emit a never-ending stream like SQLbrite does.
I have tried variations of this operator like concatArray and the result was the same, no data is being retrieved whatsoever.
An interesting note would be the fact that while debugging, I noticed that both mQuakesLocalDataSource and mQuakesRemoteDataSource get methods are triggered when passed into concat operator, before reaching the first() line. Shouldn't concat evaluate sources one by one (and filter the current one) in the presence of first operator?
I have also tried adding a filter with a Predicate in order to cache data into the Local Data Source, as below:
#NonNull
#Override
public Single<List<Quake>> getQuakes() {
return Single.concat(mQuakesLocalDataSource.getQuakes(),
mQuakesRemoteDataSource.getQuakes()).filter(new Predicate<List<Quake>>() {
#Override
public boolean test(List<Quake> quakes) throws Exception {
boolean isValid = quakes != null && !quakes.isEmpty();
// save items to local data source
if (isValid) saveQuakes(quakes);
return isValid;
}
}).first(new ArrayList<>());
}
The result was the same, no data is being retrieved.
Returning an empty list in Single doesn't make the Single empty, thus first will correctly stop at the first item, the empty list, never calling the remote source. You have to decide on the item via flatMap whether or not to resume with the remote source instead of concat:
mQuakesLocalDataSource.getQuakes()
.flatMap(list -> {
if (list.isEmpty()) {
return mQuakesRemoteDataSource.getQuakes();
}
return Single.just(list);
})
For my app, I have implemented a filter option. I use a Firebase Query to query using said filters. Here's the implementation:
private void loadDestinations() {
Query query = App.getFirestore().collection("destinations");
if (mFilters.hasCountry()) {
query = query.whereEqualTo(Destination.FIELD_COUNTRY, mFilters.getCountry());
}
if (mFilters.hasPrice()) {
query = query.whereEqualTo(Destination.FIELD_PRICE, mFilters.getPrice());
}
if (mFilters.hasSortBy()) {
query = query.orderBy(mFilters.getSortBy(), mFilters.getSortDirection());
}
query.addSnapshotListener((documentSnapshots, e) -> {
if (documentSnapshots != null) {
if (!documentSnapshots.isEmpty()) {
Log.d(LOG_TAG, "Snapshots are not null and have value");
List<Destination> destinationList = documentSnapshots.toObjects(Destination.class);
mDestinations.setValue(destinationList);
} else {
Log.d(LOG_TAG, "Snapshots are not null but have no value");
mDestinations.setValue(new ArrayList<>());
}
} else {
Log.e(LOG_TAG, "Snapshots are NULL!");
mDestinations.setValue(new ArrayList<>());
}
});
}
Take a look at the second line where I instantiate the query object. I then compound it for every type of filter (country, price, sorting) that was added by the user.
Now this loads perfectly fine if I compound country and price, but as soon as I add the sorting aka query.orderBy(), the documentSnapshots on line 17 return null. To make it even weirder, the snapshotListener retrieves data TWICE if it was with a compounded query. The first time it logs:
Snapshots are not null and have value
and then immediately afterward it logs
Snapshots are NULL!
even though the loadDestinations() method was called once (I checked).
I know for certain that this wasn't an issue a month or so ago because that's when I developed this portion and tested it out. Yet now, for reason, it is acting this way. Have there been any drastic changes to the API? Is the code at fault?
EXTRA: Say, on line 2, I add another .whereEqualTo() like so:
Query query = App.getFirestore().collection("destinations").whereEqualTo("approved", true);
The listener would return null values with just one filter added (as opposed to two before). The reason I mention this is because I don't think it's an issue with how the data is sorted but more so the fact that compounded queries don't work. And it, for some reason, retries a second time until it is null. I don't think it's about creating an index either because I already did that a month ago. Plus it would have mentioned as much in the log.
I suspect what is happening is that doing a "equals to" query on one field plus a "sort by" on another field requires your creating a composite index in order to run this query.
Normally, you'd get the URL to create this custom index in the error object, so I would highly recommend checking for, and then logging the contents of the error object.
(And even if that isn't the cause, it's always a good idea to get into the habit of checking your error object anyway. That'll probably help you avoid a bunch of mysterious bugs in the future.)
In java methods everything is passed-by-value so i can change the object attributes passed to the method and expect that the original object attributes are changed. but in this method i get different result:
I have this method:
public Observable<Menu> makeMenu(Menu menu, NumberSettingChanges.MenuChanges changes) {
// Start flow with added and edited extensions
return Observable.from(changes.added.entrySet())
.mergeWith(Observable.from(changes.edited.entrySet()))
//Upload announcement voices or do nothing if extension is not an announcement
.flatMap(e -> {
if (AppTypeContract.APP_TYPE_ANNOUNCEMENT.equals(e.getValue().type)) {
return mMediaManager.uploadAsync(e.getValue().config.localPrompt)
.doOnNext(response -> {
//Update extension prompt with the storage path.
menu.config.extensions.get(e.getKey()).config.prompt = response.mPath;
menu.config.extensions.get(e.getKey()).config.localPrompt = "";
})
.flatMap(response -> Observable.just(e));
} else {
return Observable.just(e);
}
}
)
}
and i manipulate menu attributes in the flatmap:
menu.config.extensions.get(e.getKey()).config.localPrompt = "";
I call the method in the same class:
public Observable<NumberSetting> saveSettings(NumberSetting o, NumberSetting n) {
NumberSettingChanges changes = compareNumberSetting(o, n);
return makeMenu(n.day, changes.day)
.mergeWith(makeMenu(n.night, changes.night));
}
and finally:
saveSettings(ns, mNumberSettingNew).subscribe();
What i expect is that the mNumberSettingNew.menu.config.extensions.get(e.getKey()).config.prompt is changed but no change is happening after this call and the mNumberSettingNew has no change at all.
Note that i am sure that changing prompt line is done in the debug.
I don't think I could explain Java's parameter semantics any better than (or even half as good as) the link you referenced in your first paragraph so I won't try. The main point is: Everything in Java is passed by value (i. e. copied) but with objects what is copied is not the object itself but the reference to the object. So in other words the reference is passed by value.
So with respect to your particular problem: Yes, if you pass a reference to a mutable object to some rx-java code that reference will point to the same instance of the object. If you mutate the instance then the caller code will also be able to see the changes because they were made on the same instance. That's because rx-java is still only Java and cannot change the language semantics on that level.
Without seeing the whole code I am unsure what could be the problem here... When are you checking whether mNumberSettingsNew actually has the changes you were making in your doOnNext? If you check that immediately after saveSettings(ns, mNumberSettingNew).subscribe(); your uploadAsync may not have returned yet. You could try adding an actual Subscriber in your subscribe and check the result there.
On a more general note, I think you should try to avoid side-effects like this as much as you can when using rx-java. Your case - taking an input object, applying a set of (possibly asynchronous) changes to that object, and waiting for the changed output object - is a bit tricky, but I think it could be done with scan. Maybe something vaguely like this:
Observable.from(changes.added.entrySet())
.mergeWith(Observable.from(changes.edited.entrySet()))
.scan(menuBeforeAnyChanges, new Func2<Menu, Change, Menu>() {
public Menu call(final Menu previousVersionOfTheMenu, final Change nextChange) {
// since I don't know of a version of scan that can return
// an Observable you would I think you would have to adapt
// your code in here to be fully synchronous - but of
// course the scan itself could run asynchronously
final newVersionOfTheMenu = previousVersionOfTheMenu.applyChange(nextChange);
return newVersionOfTheMenu;
}
)
This would take the original Version of the menu, consecutively apply all the changes from added and edited and /emit/ every updated version of menu. So you would not have any side effects but simply subscribe to that observable with a Subscriber<Menu> and then take the last() Menu and that would be the one with all changes applied.
EDIT: Oh, I just saw that there is another method called reduce that does just that: first scan and then last or takeLast.
I'm building an android app using the Android Parse SDK, which gets all data from Parse at initialisation and stores it locally. Later, it will only update those entities (ParseObjects) which need so. I'm not getting any return from some Pin() operations, and similarly no callback when I use PinInBackground() and variants. Same happens with Unpin().
My code is something like the following. I have a list of ControlDate, a ParseObject which contains updated_at and updated_locally_at for each Parse data table. I use it to decide if I should query a given table (reducing number of queries). I iterate over this list when I perform a data update, in an IntentService, like this:
protected void onHandleIntent(Intent intent) {
for(ControlDate d : mUpdateQueue) { // mUpdateQueue is the list of ControlDate
if(d.entityNeedsUpdate()) { // returns true if updated_at > updated_locally_at
updateEntity(d);
}
}
private boolean updateEntity(ControlDate d) {
String tableName = d.getTableName();
Date lastLocalUpdate = d.getLastLocalUpdate();
ParseQuery<ParseObject> qParse = ParseQuery.getQuery(tableName);
qParse.whereGreaterThan("updated_at", lastLocalUpdate);
try {
// update the entities
List<ParseObject> entities = qParse.find();
ParseObject.pinAll(entities); // SOMETIMES GETS STUCK (no return)
// update the associated ControlDate
d.setLastLocalUpdate(new Date()); // updated_locally_at = now
d.pin(); // SOMETIMES GETS STUCK (no return)
}
catch(ParseException e) {
// error
}
}
}
Those operations SOMETIMES do not return. I'm trying to find a pattern but still no luck, apparently it started happening when I added pointer arrays to some of the entities. Thus, I think it may be due to the recursive nature of pin(). However it is strange that it sometimes also gets stuck with ParseObjects which do not reference any others - as it is the case with d.pin().
Things I've tried:
changing the for loop to a ListIterator (as I am changing the list of ControlDates, but I don't think this is necessary);
using the threaded variants (eg.: PinInBackground()) - no callback;
pinning each entity individually (in a loop, doing pin()) - a lot slower, still got stuck;
debugging - the thread just blocks here: http://i.imgur.com/oBDjpCw.png?1
I'm going crazy with this, help would be much appreciated!
PS.: I found this https://github.com/BoltsFramework/Bolts-Android/issues/48
Its an open issue on the bolts library, which is used in the Android SDK and may be causing this (maybe?). Anyway I cannot see how I could overcome my problem even though the cause for the pin() not returning could be an "unobserved exception" leading to a deadlock.