I have a requirement wherein I have to send saved API requests on a button click. These API requests are added to a list and this list is saved to SharedPreferences if the device is offline. Once the device regains connectivity, the saved requests should be sent on a click of a button. If one of the requests get a HTTP status code of 401, the whole process should stop. However, in case of other Exceptions, the process should not interrupted and the next saved request on the list should be sent. If a request succeeds, it is removed from the list of saved requests. At the end of the process, any requests that remain unsent are saved to SharedPreferences.
Now I have a special case for an Exception that I call InvalidRequestException. I want to remove the request from the list when it encounters this particular error, and at the same time I want to carry on sending the remaining requests in the list.
I modeled my code from this post. Here is the code for the method that kicks off the whole process:
public LiveData<UploadStatus> startUploading() {
MutableLiveData<UploadStatus> uploadStatus = new MutableLiveData<>();
compositeDisposable.add(paramRepository.getSavedOfflineRequest() // returns Observable<List<Request>>
.doOnComplete(() -> uploadStatus.setValue(UploadStatus.NO_ITEMS))
.flatMapIterable( requests -> {
requestList = requests;
requestListSizeText.set(Integer.toString(requestList.size()));
return requestList;
}) // observable should now be Observable<Request>
.flatMapCompletable(this::uploadProcess)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(() ->{
paramRepository.setOfflineRequestString(""); // clear saved offline requests from shared preferences
uploadStatus.setValue(UploadStatus.SUCCESS);
},
error -> {
if (error instanceof SessionExpiredException) {
uploadStatus.setValue(UploadStatus.LOGGED_OUT);
} else {
if(!requestList.isEmpty()) {
paramRepository.saveRequestsToPrefs(requestList);
} else {
paramRepository.deleteSavedRequests();
}
uploadStatus.setValue(UploadStatus.FAIL);
}
}
)
);
return uploadStatus;
}
The actual sending of saved requests happens in uploadProcess. This is where I attempt to catch the occurrence of InvalidRequestException and delete the request that encounters it:
private Completable uploadProcess(Request request) {
return apiService.transact(saleUrl, BuildConfig.ApiKey,request)
.doOnSubscribe(disposable -> {
uploadAttempts++;
})
.toMaybe()
.onErrorResumeNext(error -> {
if(error instanceof InvalidRequestException) {
requestList.remove(request);
if(requestList.isEmpty()) {
return Maybe.error(new OfflineTxnsNotUploadedException());
}
}
else if (error instanceof SessionExpiredException) // inform UI that session has expired
return Maybe.error(error);
else if (requestList.size() == uploadAttempts) { // nothing was uploaded
return Maybe.error(new OfflineTxnsNotUploadedException());
}
return Maybe.empty();
})
.flatMapCompletable(response -> {
requestList.remove(request);
successCount++;
successCountText.set(Integer.toString(successCount));
return createTransaction(request, response);
});
}
Now when I tested this, I found out that the whole stream stops whenever InvalidRequestException is encountered, which is not the behavior I want. I want to continue sending the other requests in the list. I actually removed the part where the request is removed from the list (requestList.remove(request);), and the stream continued and the next request was sent via apiService.transact().
Am I mistaken in assuming that returning Maybe.empty() would resume the emission of Observable<Request> from the flatMapIterable?
EDIT: It seems I am encountering a ConcurrentModificationException, that's why the stream terminates immediately and the other Requests are not sent. I will have to study this exception first.
As I noted in my edit, I wasn't able to catch the ConcurrentModificationException, thus the entire stream was interrupted. I was in fact, modifying the List<Request> that is being emitted into individual Observable<Request>, since requestList is only a shallow copy of the List<Request> emitted by getSavedOfflineRequest.
One solution I tried was to first serialize the list via Moshi, so that the unserialzied list will not hold any reference to the original list. I found out soon enough that requestList.remove(request) will return false, since I had yet to properly override the equals() and hashCode() method for the Request class. In the end, I just settled to create a "failed requests" list, adding Requests to this list whenever an error is encountered.
Related
I am retrieving my response from my API with rxjava with funcs as follows:
#Override
public Single<MyInfoResponse> getMyInfoApiCall() {
return Rx2AndroidNetworking.get(ApiEndPoint.ENDPOINT_MY_INFO)
.addHeaders(mApiHeader.getProtectedApiHeader())
.build()
.getObjectSingle(MyInfoResponse.class);
}
Now ,I am retrieving this data and using it in my UI code( with usual compositedisposables) as follows:
#Override
public void onViewPrepared() {
getCompositeDisposable().add(getDataManager()
.getMyInfoApiCall()
.subscribeOn(getSchedulerProvider().io())
.observeOn(getSchedulerProvider().ui())
.subscribe(myInfoResponse -> {
getDataManager().updateMyManagerInfo(myInfoResponse);
if (myInfoResponse != null && myInfoResponse.getData() != null) {
getMvpView().updateMyRepo(myInfoResponse.getData());
}
}, throwable -> {
if (!isViewAttached()) {
return;
}
getMvpView().hideLoading();
// handle the error here
if (throwable instanceof ANError) {
ANError anError = (ANError) throwable;
handleApiError(anError);
}
}));
}
Now everytime I have internet connectivity and it tries to retrieve data it works fine, but as soon as I lose internet connectivity, I am trying to cache this data so it still displays on the UI until the network connectivity is back and ready to update data real time. How do I go about this in the easiest and the most ideal way possible?
I suggest you to use offline support using android Room Persistence.
Have a look at the google sample from the below link
https://developer.android.com/topic/libraries/architecture/room
In this case
Prefetch a request (so that it can return from cache when required at instant)
AndroidNetworking.get("https://fierce-cove-29863.herokuapp.com/getAllUsers/{pageNumber}")
.addPathParameter("pageNumber", "0")
.addQueryParameter("limit", "30")
.setTag(this)
.setPriority(Priority.LOW)
.build()
.prefetch();
First of all the server must send cache-control in header so that is
starts working.
Response will be cached on the basis of cache-control
max-age,max-stale.
If internet is connected and the age is NOT expired it will return
from cache.
If internet is connected and the age is expired and if server returns
304(NOT MODIFIED) it will return from cache.
If internet is NOT connected if you are using
getResponseOnlyIfCached() - it will return from cache even it date is
expired.
If internet is NOT connected , if you are NOT using
getResponseOnlyIfCached() - it will NOT return anything.
If you are using getResponseOnlyFromNetwork() , it will only return
response after validation from server.
If cache-control is set, it will work according to the
max-age,max-stale returned from server.
If internet is NOT connected only way to get cache Response is by
using getResponseOnlyIfCached().
I just went through the docs:
Refer here
I am developing an Android app.
RxJava is used.
It stores user data in the local database with expire time.
First, it gets user data from the local database.
And check the expire time, if the data is old data, then it gets user data from remote server and update it to the local database.
fun getPlayer(playerId: String): Single<Player> {
return playerDao.getPlayer(playerId)
.doOnSuccess { // "doOnSuccess" is right? what method should I use?
if (PlayerUtil.isNeededUpdate(it)) {
Log.d(TAG, "getPlayer(local) - old!")
getPlayerFromRemote(playerId)
// How can I return Observable/Flowable/Single in here?
// (case of need to update data)
}
}
.onErrorResumeNext {
// If local database has no player, then it try to get it from remote server
Log.d(TAG, "getPlayer(local) - onError: ${it.message}")
getPlayerFromRemote(playerId)
}
}
doOnSuccess is meant to be used for side-effects, not actions that impact the stream itself.
What you're looking for is flatMap and just returning the scalar value if nothing needs to be done:
.flatMap {
if (PlayerUtil.isNeededUpdate(it)) {
getPlayerFromRemote(playerId)
} else {
Single.just(it)
}
}
I'm trying to use RxJava with Android to asynchronously update my view. When user clicks the movie from the list in the RecyclerView, I want to present him first with the movie from the database, if present. Then I want to fetch the latest information and update the database as well as UI. I'm trying to use concat method and its variant but it does not work.
I have skipped other codes only to post the relevant RxJava methods that are fetching data as the rest is working fine.
When I disable network connection with the code below (hence remote returns error), the code below does not display data from the database at all. Only it reports the error. Which means the local is not resolving.
public Flowable<Movie> getMovie(final int id) {
return Single.concat(mLocal.getMovie(id), mRemote.getMovie(id).doOnSuccess(data -> {
mLocal.save(data);
})).onErrorResumeNext(error->{
return Flowable.error(error);
});
}
And in this code, it works fine, except now that I don't get the error message (and rightly so, since I have replaced it with new stream from the database)
public Flowable<Movie> getMovie(final int id) {
return Single.concat(mLocal.getMovie(id), mRemote.getMovie(id).doOnSuccess(data -> {
mLocal.save(data);
})).onErrorResumeNext(error->{
return mLocal.getMovie(id).toFlowable();
});
}
Now, how can I get database data first and then fire network call next to update data and get errors from the database or network call?
UPDATE
The latest method code
// calling getMovie on mLocal or mRemote returns Single
public Flowable<Movie> getMovie(final int id) {
return Single.concat(mLocal.getMovie(id), mRemote.getMovie(id).doOnSuccess(data -> {
mLocal.insertMovie(data);
})).onErrorResumeNext(error -> {
return Flowable.error(error);
});
}
Here is how I call them
public void loadMovie(int id)
{
Disposable d = mRepo.getMovie(id)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread(), true)
.doOnSubscribe(subscription -> {
subscription.request(Long.MAX_VALUE);
//post progress here
})
.subscribe(data -> {
//onNext
},
error -> {
//onError
},
() -> {
//onComplete
}
);
mDisposables.add(d);
}
With affirmation that my code works and guides on troubleshooting from #akarnokd I found the latest code (see OP) works flawlessly. The result of RxJava chain is posted to LiveData object which should update View. Unfortunately it only posts the latest data (which is an error) and skips the first (which is the data from the database).
I will deal with that but since the post deals with RxJava, I will consider this solved!
I am trying to implement a refresh token flow in RxJava2 and Kotlin and I have trouble handling errors. There are several requests that need to be done in sequence but the sequence differs in case there are some errors.
Basically, if I try to use my refresh token and receive 400 - Bad Requestresponse because the token is not valid, I need to terminate the flow and do not execute the next switchMap (and ideally, I would like to return the final Observable<ApplicationRoot>). But I am not sure how to achieve this.
If I use onErrorReturn, I will just pass the returned result to the next switch map. And doOnError just executes the step when the request fails but the whole sequence continues.
fun refreshToken(): Observable<ApplicationRoot> {
// try to use the refresh token to obtain an access token
return authRepository.refreshToken(token)
.switchMap { response ->
// process response here
userRepository.getUser() // fetch user details
}.doOnError {
// TODO - return final result, do not jump to next switchMap
// refresh token is not valid -> go to login screen
Observable.just(ApplicationRoot.LOGIN) // not working
}.switchMap { response -> // excpects response of type UserResponse
// save user details here
}
}
Does anyone know who to jump out of the sequence of switch maps if some error occurs?
Probably, you should do something like this:
fun refreshToken(): Observable<ApplicationRoot> {
return authRepository.refreshToken(token)
.switchMap { response ->
userRepository.getUser()
}
.switchMap { response -> // do something with user response
}
.map { ApplicationRoot.ROOT_THAT_MEANS_SUCCESS }
.onErrorResumeNext(Observable.just(ApplicationRoot.LOGIN))
}
I am not aware about implementation of authRepository.refreshToken and how do responses look like, but in case if response is your custom object rather than retrofit2.Response<T> it should work.
i am building my app on android repository by Fernando Cejas and i have a problem with subscribing to observable after calling dispose.
When i come to dashboard, i call method subscribeOnUserMessages.execute(new Subscriber(), new Params(token)), which is method in UseCase class
public void execute(DisposableObserver<T> observer, Params params) {
Preconditions.checkNotNull(observer);
final Observable<T> observable = this.buildUseCaseObservable(params)
.subscribeOn(Schedulers.from(threadExecutor))
.observeOn(postExecutionThread.getScheduler());
addDisposable(observable.subscribeWith(observer));
}
In child class SubscribeOnUserMessages i simply call repository like this
return messageRepository.subscribeOnUserMessages(params);
In my socket implementation i create like this
return Observable.create(emitter -> {
if (!isThereInternetConnection()) {
Timber.w("Network connection exception");
emitter.onError(new NetworkConnectionException());
return;
}
/*
* Open socket if not opened
*/
openSocket(params.getToken());
String channelName = CHANNEL_PRIVATE_USER + params.getAuthenticated().getUuid();
if (subscribedChannels.contains(channelName)) {
Timber.d("Channel %s is already subscribed", channelName);
return;
}
JSONObject auth;
try {
auth = createAuthJson(CHANNEL, channelName, params.getToken());
} catch (JSONException e) {
Timber.e("Couldn't create auth json");
emitter.onError(e);
return;
}
mSocket.emit(SUBSCRIBE, auth);
Timber.d("Emitted subscribe with channel: %s ", CHANNEL_PRIVATE_USER + params.getAuthenticated().getUuid());
subscribedChannels.add(CHANNEL_PRIVATE_USER + params.getAuthenticated().getUuid());
Timber.d("Subscribing on event: %s\n with user: %s", EVENT_USER_NEW_MESSAGE, params.getAuthenticated().getUuid());
if (mSocket.hasListeners(EVENT_USER_NEW_MESSAGE)) {
Timber.v("Socket already has listener on event: %s", EVENT_USER_NEW_MESSAGE);
return;
}
mSocket.on(EVENT_USER_NEW_MESSAGE, args -> {
if (args[1] == null) {
emitter.onError(new EmptyResponseException());
}
Timber.d("Event - %s %s", EVENT_USER_NEW_MESSAGE, args[1].toString());
try {
MessageEntity messageEntity = messageEntityJsonMapper.transform(args[1]);
emitter.onNext(messageEntity);
} catch (JSONException e) {
Timber.e(e, "Could not parse message json");
emitter.onError(e);
}
});
});
Symptoms are that first time i subscribe everything is going through to presentation layer. When i dispose after going to second screen and come back i only see logs coming to socket implementation, but not going through.
My question is: Is there a method for subscribing to same observable again? I've already tried to save that observable in my use case in singleton and subscribe to that observable, didn't help.
Without additional info and details regrading socket implementation it is hard to spot the problem exactly, but, from the code you've posted, you don't have dispose logic, so while you might properly call dispose() to the Observable at the correct lifecycle event, your socket will actually stay open, and it might not got disconnected/closed properly ever.
That might lead to a problems opening and connecting to the socket at the 2nd time, as you might try to reopen already open socket and depends on your internal socket impl that might be a problem.
(I can see in the comment that openSocket if not already opened, but still there might be problem elsewhere calling some method on the socket multiple times or setting listeners, again depends on the socket impl)
As a general guidelines, you should add dispose logic using emitter.setCancellable()/emitter.setDisposable() in order to dispose properly the socket resources when you no longer need them, thus - when applying subscribe again (whether the same object or not) will invoke your subscription logic again that will reopen the socket and listen to it.
It is not clear to me if you like to keep the socket open when you moving to a different screen (I don't think it is a good practice, as you will keep this resource open and might never get back to the screen again to use it), but if that's the case as #Phoenix Wang mentioned, you can use publish kind operators to multicast the Observable, so every new Subscriber will not try to reopen the socket (i.e. invoking the subscription logic) but will just get notify about messages running in the already opened socket.