I have an API that returns FamilyDetails. I use retrofit to fetch them, and it is modelled as follows:
Observable<FamilyDetails> getFamilyDetails(#Query int personId);
Once fetched, those details will be used to update a Person. My API delivers those FamilyDetails as soon as it has some information to show, which means that the details may be incomplete, and thus there is a complete = false flag delivered in responses when the server hasn't finished fetching all family details - in turn, my app should send another request to fetch the remaining FamilyDetails if we receive complete = false in the first response, and keep doing so for as long as the details remain incomplete.
I have "somewhat" achieved what I wanted, but not entirely with this code:
personApiService.getFamilyDetails(personId)
// repeat call
.flatMap(familyDetails -> personApiService.getFamilyDetails(personId))
// until family details are complete
.takeUntil(familyDetails -> familyDetails.isComplete())
// update the person object with the family details
.flatMap(familyDetails -> Observable.just(updatePerson(familyDetails))
//subscribe!
.subscribe(personSubscriber);
My personSubscriber returns an updated Person.
The problem I have with this implementation is that partial updates don't go through personSubscriber's onNext(), as I only get one call to onNext() with the updated Person object with its complete FamilyDetails.
I would like to model this using RxJava with the following requirements:
if I receive incomplete details, I update the Person
object those details belong to, and deliver it via onNext().
If we receive incomplete details, we keep querying the API for complete details, and deliver updated Person objects via onNext() in the same observer.
Once we get the last onNext() call with the updated Person, then onComplete() is called and we are done.
Thanks in advance!
personApiService.getFamilyDetails(personId)
// repeat call
.repeat()
// until family details are complete
.takeUntil(familyDetails -> familyDetails.isComplete())
//subscribe (do your updates here)
.subscribe(personSubscriber);
Related
I am investigating the use of RxJava in my latest Android application.
I have a two lists of related updated Database model objects
ListDB1 and ListDB2
the logic I am attempting to implement is as follows
1). For each item in ListDB1
1.1). Transform it to a Network model object
1.2). Execute an Update RESTful API
2). Once all network updates have completed successfully
2.1). Persist ListDB1 to my local database.
2.2). Persist ListDB2 to my local database.
So far I have this code which should call my network API's
Observable.just(getDatabaseList())
.subscribeOn(Schedulers.io())
.flatMapIterable(x -> x)
.flatMap(database -> transformDatabase(database, DB_1_MAPPER))
.doOnNext(NetworkController::updateRemote)
.observeOn(AndroidSchedulers.mainThread())
.doOnComplete(getOnComplete())
.doOnError(getOnError())
.subscribe();
No API calls are executed though
I would rather use Single that Observable as my API calls respond with Single<Response>, however I couldnt see how to achieve Observable.just(<>) with a Single.
Also I cannot see how to commence my Rx process by processing each List item separately for the Network calls, then performing Database calls with a complete list as I am using Realm as my local database which can accept lists of database objects in a single update.
In pseudo code my process resembles:
for each database list item
convert to network model item
call remote update API
When all all network calls are successful
update database with ListDB1
update database with ListDB2
end
Is this possible in one Rx process "stream"?
1.
I would rather use Single that Observable as my API calls respond with Single, however I couldnt see how to achieve Observable.just(<>) with a Single.
You can do this:
Single.just(getDatabaseList()) // Single<>
However, Single is not suitable in this case because you are not working with a single item, and what you need instead is to iterate through multiple items and work on the items one by one.
2.
processing each List item separately for the Network calls, then performing Database calls with a complete list
You can use toList() operator which emits entire list of items when the observable completes.
3.
The purpose of do operators such as doOnNext, doOnComplete, and doOnError is to create side effect that does not affect the stream. An example of this kind of operations is logging. You should not do any meaningful operation that affect the stream in such operators.
Instead you should be using operators such as map, flatMap, etc.
4.
Putting everything together:
Observable.fromIterable(getDatabaseList())
.subscribeOn(Schedulers.io())
.flatMap(database -> transformDatabase(database, DB_1_MAPPER))
.flatMap(NetworkController::updateRemote)
.toList() // This has type of Single<List<>>
.flatMap(list -> {
// Update db1 and db2 with the result of server update.
return Single.zip(updateDb1(list), updateDb2(list), (a, b) -> {
// Combine result of db updates
});
})
.observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> {
// Handle success case
}, err -> {
// Handle error case
});
I am working with WorkManager Alpha 05.
I'm developing a Service that enqueues task on demand of other applications.
It has two methods:
createTask (Create a new task, given a name and a set of data, it returns and ID)
checkTaskStatus (The application asks the services given a ID, the status of the task)
The communication is done via bound services using messages. That means both client and services has the correct implementations to communicate information.
Method 1 is working fine.
I have problems with method 2.
WorkManager.getInstance().getStatusById(taskID)
.observe(LifecycleOwner, Observer {
status -> if (status !=null){
val myResult = status.state.toString()
statusString = myResult
Log.d("Task Status",myResult)
}
})
The observer is logging the status correctly, but I can't send back that message to the client. Is there a way to check the status in a sync way?
I don't really need to have the task attached to a LiveData.
Seems like SynchronousWorkManager was removed on October 11:
Removed WorkManager.synchronous() and WorkContinuation.synchronous() and all related methods. Added ListenableFuture as the return type of many methods in the API. This is a breaking API change.
How to use ListenableFuture:
You can now synchronously get and observe by using ListenableFutures. For example, WorkManager.enqueue() used to return void; it now returns a ListenableFuture. You can call ListenableFuture.addListener(Runnable, Executor) or ListenableFuture.get() to run code once the operation is complete.
More info can be found here.
The WorkManager instance has a synchronous method which returns the SynchronousWorkManager, This will give you a set of methods to perform synchronous operations. Take into account that this is meant to be used in a background thread.
I have a subscription that wait for the push notification and another one that is polling the server to get response. I want to start both observable together and return the data from the one which finish first. What would be operator to use here?
Since you want to have the data of the first one to finish, you have to put the data somewhere until you get to the terminal event by collecting each into its own list and using amb that picks the source that signals an event (the collected list) first. Then you can unroll the list back to individual items.
Observable<A> source1 = ...
Observable<A> source2 = ...
Observable.amb(source1.toList(), source2.toList())
.flatMapIterable(list -> list)
.subscribe(...);
The operator you are looking for is first. Of-course, you'll have to merge the Observables first (by using merge, or probably better - mergeDelayError, so if only one of them fails, you'll still get the first which finishes with a vaild result).
Should look like:
Observable.mergeDelayError(pushObservable, pullObservable)
.first()
.subscribe(data->...);
I'm using Firebase Remote Config to fetch remote data and my app needs an up-to-date data from the first launch.
I'm doing a fetch and update in my Application's onCreate():
mFirebaseRemoteConfig.fetch(cacheExpiration)
.addOnCompleteListener(new OnCompleteListener<Void>() {
#Override
public void onComplete(#NonNull Task<Void> task) {
if (task.isSuccessful()) {
mFirebaseRemoteConfig.activateFetched();
}
}
});
And read the value with :
myValue = mFirebaseRemoteConfig.getBoolean(Constants.FIREBASE_REMOTE_MY_VALUE);
The first fetch works well (activateFetched() is successfully triggered), but it returns the remote_config_defaults value and not the published remote config.
The second fetch, even a few seconds later, returns the remote value.
After that, the following fetches are subject to the cacheExpiration rule (which is totally OK).
Any idea why my remote value is not fetched at the first call?
It sounds like you are overlooking the asynchronous nature of fetching the remote parameters. The onComplete() callback fires after a request to the Firebase servers is sent and the reply received. This will take a fraction of a second, maybe more.
If your statement to use the fetched value:
myValue = mFirebaseRemoteConfig.getBoolean(Constants.FIREBASE_REMOTE_MY_VALUE);
follows the call to fetch() and is not in the onComplete() callback, it will execute before the config data has been received. The second call only appears to work because enough time has elapsed for the first call to complete and the data it fetched and activated is present.
The callbacks for Firebase Remote Config have been designed like that, it will return the cached values first. If there is no cached value saved from the server, it will return the value defined in defaults and trigger a remote fetch. The next time it returns it will return the fetched values from the server if it manages to save them.
The way in which Firebase Remote Config decides on a value can be described as follows:
First it checks if there is a cached value that was stored from the server, if there is it uses that and will return that value on the first call.
If there is no cached value, it looks to the defaults defined either programmatically or in the defaults file. (When you call setDefaults())
If there is no value cached from the server, and no value in defaults, it uses the system default for that type.
More info can be found here : https://firebase.google.com/docs/remote-config/
Like #Bob Snyder pointed out, this is because of the async nature of firebase.
So use onCompleteListener like this to fix the issue:
firebaseRemoteConfig.activate().addOnCompleteListener {
//logic to check the remote value
}
One issue that I was running into when fetching the RemoteConfig from an Android device was that we were initially using the method
fetch()
which gave us the same issue where the initial value was always the same as the default. Changing this to
fetchAndActivate()
fixed the issue for us. I assume the difference is that Firebase allows you to fetch the data but not immediately 'activate' it, which presumably is helpful if you want to take some immediate action based on your default values, then activate the remote values and then any logic after that point would be based on the remote values.
Hope this helps someone :)
I'm saving the user's location in the app local database and then send it to the server. Once the server return a success, I delete the location that was sent.
Each time a point has been saved in the database I call this method:
public void sendPoint(){
amazonRetrofit.postAmazonPoints(databaseHelper.getPoints())
.map(listIdsSent -> deleteDatabasePoints(listIdsSent))
.doOnCompleted(() -> emitStoreChange(finalEvent))
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(AndroidSchedulers.from(backgroundLooper))
.subscribe();
}
I query the database for the point to be send to the server
I received from the server the list of point successfully sent
Using .map(), I gather the point successfully sent and delete them from the local database
Sometimes, It happens that I call this method repeatedly without having wait for the previous request to be completed and deleted the point sent. So, when I call that method again, it will post the same point as the previous request because that previous request isn't completed yet thus haven't deleted the point using the .map() yet. Causing the server to receive duplicates...
Timeline
1st Call to postPoint()
Retrive point A,B,C from the database
Post point A,B,C to the server
2nd call to postPoint()
Retrive point A,B,C,D from the database
Post point A,B,C,D to the server
Receive success from the 1st request
Deleting A,B,C from the local database
Receive success from the 2nd request
Deleting A,B,C,D from the local database
Result:
The server database now have received : A,B,C,A,B,C,D
Each request occurs sequentially but somehow the same location points are sent to the server when I call sendPoint() too quickly. How can I fix this?
First to all you are not using observerOn operator properly, observeOn operator is applied over the steps in your pipeline, once is defined.
So if you define at the end of the pipeline just before subscribeOn, then none of your previous steps will be executed in that thread.
Also, since you need to wait until the response of your server call, you can use the callbacks handlers that Subscriber already provide (onNext(), onComplete())
public void sendPoint(){
Observable.from(databaseHelper.getPoints())
.observeOn(AndroidSchedulers.mainThread())
.flatMap(poins-> amazonRetrofit.postAmazonPoints(points))
.subscribeOn(AndroidSchedulers.from(backgroundLooper))
.subscribe(listIdsSent-> deleteDatabasePoints(listIdsSent), () -> emitStoreChange(finalEvent));
}
if you want to see more examples of ObserverOn and SubscribeOn you can take a look here. https://github.com/politrons/reactive/blob/master/src/test/java/rx/observables/scheduler/ObservableAsynchronous.java
You should have some kind of validation on the client side or/and on the backend side.
Client side:
The simplest solution is to add two columns to the table with locations like "processing" and "uploaded".
When you select locations from database and clausure where processing=false and uploaded=false.
Then when you have rows ready to sent set processing=true and when the server returns success set done=true.
Backend side (optional, depends on requirements):
You should send location with timestamp to the server (probably one more additional column in your client side table). If the server gets a location with timestamp older than the last one in a database it shouldn't store it.
RxJava solution:
You can implement a similar solution with memory cache which is kept around all sendPoint as List.
Pseudocode:
public void sendPoint(){
databaseHelper.getPoints()
.filter(points -> pointsNotInCache())
.map(points -> amazonRetrofit.postAmazonPoints())
.map(points -> addToCache())
.map(listIdsSent -> deleteDatabasePoints(listIdsSent))
.map(listIdsSent -> removeSentPointsFromCache()) //if you would like save memory
.doOnCompleted(() -> emitStoreChange(finalEvent))
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(AndroidSchedulers.from(backgroundLooper))
.subscribe();
}
It looks like, as everyone else is saying, you need an intermediate cache.
i.e.
HashSet<Point> mHashSet = new HashSet<>();
public void sendPoint() {
Observable.from(databaseHelper.getPoints())
.filter(point -> !mHashSet.contains(point))
.doOnNext(mHashSet::put)
.toList()
.flatMap(amazonRetrofit::postAmazonPoints)
.map(this::deleteDatabasePoints)
.doOnCompleted(() -> emitStoreChange(finalEvent))
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(AndroidSchedulers.from(backgroundLooper))
.subscribe();
}