I have a following class with RxJava implementation for download two resources from API. I do some rx to allow it retry or repeat when it don't meet api/connection requirements. However, I cannot make retryWhen() fire onError() after attempt more than 3 times.
QUESTION / HELP
please, review my code and help me solve this issue.
NOTE
I'm implement Rx by read these two articles.
article 1, article 2
SplashPresenter.class
public class SplashPresenter implements SplashContract.Presenter {
private static final String TAG = SplashPresenter.class.getName();
private static final int RETRY_TIMEOUT = 10;
private static final int STOP_RETRY_TIME = 3;
private static final int START_RETRY_TIME = 1;
private SplashContract.View mView;
#Override
public void init(SplashContract.View view) {
this.mView = view;
}
#Override
public void onResume() {
GetRemoteReceiverRelationshipSpec relationSpec = new GetRemoteReceiverRelationshipSpec();
GetRemoteIncompleteReasonSpec reasonSpec = new GetRemoteIncompleteReasonSpec();
Observable<RepoResult<ArrayList<IncompleteReasonViewModel>>> queryReason =
Repository.getInstance().query(reasonSpec);
Repository.getInstance().query(relationSpec)
.concatMap(result -> queryReason)
.repeatWhen(complete -> complete
.zipWith(Observable.range(START_RETRY_TIME, STOP_RETRY_TIME), (v, i) -> i)
.flatMap(repeatCount -> {
Log.i(TAG, "Repeat attempt: " + repeatCount);
mView.showLoadingDialog();
return Observable.timer(RETRY_TIMEOUT,
TimeUnit.SECONDS);
}))
.takeUntil(RepoResult::isSuccess)
.retryWhen(error -> error
.zipWith(Observable.range(START_RETRY_TIME, STOP_RETRY_TIME), (v, i) -> i)
.flatMap(retryCount -> {
Log.i(TAG, "Retry attempt: " + retryCount);
mView.showLoadingDialog();
if (mView.getCommunicator() != null) {
mView.getCommunicator().onConnectionFail(retryCount);
}
return Observable.timer(RETRY_TIMEOUT,
TimeUnit.SECONDS);
}))
.filter(RepoResult::isSuccess)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
result -> Log.i(TAG, "onNext()"),
err -> {
Log.i(TAG, "onError()");
if (mView.getCommunicator() != null) {
mView.dismissLoadingDialog();
mView.getCommunicator().onSplashScreeDismissError();
}
},
() -> {
Log.i(TAG, "onComplete()");
if (mView.getCommunicator() != null) {
mView.dismissLoadingDialog();
mView.getCommunicator().onSplashScreenSuccessDismiss();
}
}
);
}
#Override
public void onPause() {
}
}
To retain the throwable after retrying (instead of emitting a custom one), return an Observable with the appropriate error from the zipWith operator when retryCount is over the specified limit.
.retryWhen(error -> {
Observable<Integer> range = Observable.range(START_RETRY_TIME, STOP_RETRY_TIME);
Observable<Observable<Long>> zipWith = error.zipWith(range, (e, i) ->
i < STOP_RETRY_TIME ?
Observable.timer(i, TimeUnit.SECONDS) :
Observable.error(e));
return Observable.merge(zipWith);
});
When I wrote similar code before, I had manually threw Observable.error() in flatMap
.flatMap(retryCount -> {
if (retryCount >= STOP_RETRY_TIME) {
return Observable.error(someException);
}
return Observable.timer(RETRY_TIMEOUT, TimeUnit.SECONDS);
}))
Pass the retry counter and error into a Pair object if you've reached the retry limit get the exception from pair object and raise it.
source.retryWhen(errors -> errors
.zipWith(Observable.range(1, REQUEST_RETRY), Pair::new)
.flatMap(errPair -> {
int retryTime = errPair.second;
if (retryTime < REQUEST_RETRY) {
return Observable.timer(retryTime * RETRY_DELAY, TimeUnit.MILLISECONDS);
} else {
return Observable.error(errPair.first);
}
})
.doOnError(this::handleError));
Related
I have a function that takes an article id list to set on the adapter. Everything works fine until at least one of the requests fails. Then the returned list is empty. How to make it ignore a failing request and move on to the next one? For example, I request 5 articles 1 fail, 4 are okay, so I get a list of 4.
I know, I need to use onErrorResumeNext() here, but I don't know-how.
Interface:
#GET("articles/{id}")
Observable<Articles> getArticle1(#Path("id") int id);
Activity:
private void getMoreArticles(List<Integer> l) {
ApiInterface apiInterface = ApiClient.getApiClientRX().create(ApiInterface.class);
List<Observable<?>> requests = new ArrayList<>();
for (int id : l) {
requests.add(apiInterface.getArticle1(id));
}
Observable.zip(requests, new Function<Object[], List<Articles>>() {
#Override
public List<Articles> apply(#NonNull Object[] objects) {
List<Articles> articlesArrayList = new ArrayList<>();
for (Object response : objects) {
articlesArrayList.add((Articles) response);
}
return articlesArrayList;
}
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.onErrorResumeNext(Observable.<List<Articles>>empty())
.subscribe(
new Consumer<List<Articles>>() {
#Override
public void accept(List<Articles> articlesList) {
adapter = new Adapter(articlesList, MainActivity.this);
if (fav) recyclerView.setAdapter(adapter);
else addRV().setAdapter(adapter);
adapter.notifyDataSetChanged();
initListener();
swipeRefreshLayout.setRefreshing(false);
}
},
new Consumer<Throwable>() {
#Override
public void accept(Throwable e) throws Exception {
}
}
).isDisposed();
}
I tried to simplify your use case a bit but I hope you got my point. You need to somehow "signal" that there was some problem in your API call and this specific Articles object should be skipped in your .zip() operator's zipper function. You can for example wrap the return value into Optional. When the value is preset, it indicates everything went fine. If not, the API call failed.
class SO69737581 {
private Observable<Articles> getArticle1(int id) {
return Observable.just(new Articles(id))
.map(articles -> {
if (articles.id == 2) { // 1.
throw new RuntimeException("Invalid id");
} else {
return articles;
}
});
}
Observable<List<Articles>> getMoreArticles(List<Integer> ids) {
List<Observable<Optional<Articles>>> requests = new ArrayList<>();
for (int id : ids) {
Observable<Optional<Articles>> articleRequest = getArticle1(id)
.map(article -> Optional.of(article)) // 2.
.onErrorReturnItem(Optional.empty()); // 3.
requests.add(articleRequest);
}
return Observable.zip(requests, objects -> {
List<Articles> articlesArrayList = new ArrayList<>();
for (Object response : objects) {
Optional<Articles> optionalArticles = (Optional<Articles>) response;
optionalArticles.ifPresent(articlesArrayList::add); // 4.
}
return articlesArrayList;
});
}
}
Explanation of interesting parts:
Simulate API error with id = 2
Wrap result of API the call into optional
Return empty optional when an error occurs
Add articles value into result array if the value is present
Verification:
public class SO69737581Test {
#Test
public void failedArticleCallsShouldBeSkipped() {
SO69737581 tested = new SO69737581();
TestObserver<List<Articles>> testSubscriber = tested
.getMoreArticles(Arrays.asList(1, 2, 3, 4))
.test();
List<Articles> result = Arrays.asList(
new Articles(1),
new Articles(3),
new Articles(4)
);
testSubscriber.assertComplete();
testSubscriber.assertValue(result);
}
}
For sake of completeness, this is how I defined Article class:
class Articles {
public int id;
public Articles(int id) {
this.id = id;
}
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Articles articles = (Articles) o;
return id == articles.id;
}
#Override
public int hashCode() {
return Objects.hash(id);
}
}
I have successfully implemented a PagedList.BoundaryCallback, which loads a list of upcoming movies from "themoviedb" database, and saves the response into the database.
But it does not work the way I want it. Since the request return a list of upcoming movies, the response changes frequently. But if I already have data in my database, the onZeroItemsLoaded() method is not called.
My question is, how can I force the data source, or this boundary callback to always make an api request, and refresh the content of my database from the network?
public class UpcomingMoviesBoundaryCallback extends PagedList.BoundaryCallback<MovieListItemEntity> {
public static final String TAG = UpcomingMoviesBoundaryCallback.class.getSimpleName();
private UpcomingMoviesRepository upcomingMoviesRepository;
private int page = 1;
public UpcomingMoviesBoundaryCallback(UpcomingMoviesRepository upcomingMoviesRepository) {
this.upcomingMoviesRepository = upcomingMoviesRepository;
}
#Override
public void onZeroItemsLoaded() {
super.onZeroItemsLoaded();
Log.d(TAG, "onZeroItemsLoaded: ");
load();
}
#Override
public void onItemAtEndLoaded(#NonNull MovieListItemEntity itemAtEnd) {
super.onItemAtEndLoaded(itemAtEnd);
Log.d(TAG, "onItemAtEndLoaded: ");
load();
}
#SuppressLint("CheckResult")
private void load() {
upcomingMoviesRepository.getUpcoming(page)
.doOnSuccess(result -> {
upcomingMoviesRepository.saveUpcomingMovies(result);
page = result.getPage() + 1;
})
.subscribeOn(Schedulers.io())
.subscribe(result -> {
Log.d(TAG, "load: " + result);
}, error -> {
Log.d(TAG, "load: error", error);
});
}
}
public class UpcomingMoviesRepositoryImpl implements UpcomingMoviesRepository {
private static final String TAG = UpcomingMoviesRepository.class.getSimpleName();
private MovieResponseMapper movieResponseMapper = new MovieResponseMapper();
private MovieAppApi mMovieAppApi;
private UpcomingDao mUpcomingDao;
public UpcomingMoviesRepositoryImpl(MovieAppApi mMovieAppApi, UpcomingDao mUpcomingDao) {
this.mMovieAppApi = mMovieAppApi;
this.mUpcomingDao = mUpcomingDao;
}
#Override
public Single<MovieListResponse> getUpcoming(int page) {
return mMovieAppApi.upcoming(page);
}
#Override
public Single<MovieListResponse> getUpcoming() {
return mMovieAppApi.upcoming();
}
#Override
public void saveUpcomingMovies(MovieListResponse movieListResponse) {
Executors.newSingleThreadExecutor().execute(() -> {
long[] inseted = mUpcomingDao.save(movieResponseMapper.map2(movieListResponse.getResults()));
Log.d(TAG, "saveUpcomingMovies: " + inseted.length);
});
}
#Override
public LiveData<PagedList<MovieListItemEntity>> getUpcomingLiveData() {
PagedList.Config config = new PagedList.Config.Builder()
.setEnablePlaceholders(true)
.setPageSize(12)
.build();
DataSource.Factory<Integer, MovieListItemEntity> dataSource = mUpcomingDao.upcoming();
LivePagedListBuilder builder =
new LivePagedListBuilder(dataSource, config)
.setBoundaryCallback(new UpcomingMoviesBoundaryCallback(this));
return builder.build();
}
}
Inside the repository you can query database to check if data is old then you can start an async network call that will write the result directly to the database. Because the database is being observed, the UI bound to the LiveData<PagedList> will update automatically to account for the new dataset.
#Override
public LiveData<PagedList<MovieListItemEntity>> getUpcomingLiveData() {
if(mUpcomingDao.isDatasetValid()) //Check last update time or creation date and invalidate data if needed
upcomingMoviesRepository.getUpcoming()
.doOnSuccess(result -> {
upcomingMoviesRepository.clearUpcomingMovies()
upcomingMoviesRepository.saveUpcomingMovies(result);
})
.subscribeOn(Schedulers.io())
.subscribe(result -> {
Log.d(TAG, "load: " + result);
}, error -> {
Log.d(TAG, "load: error", error);
});
}
I'm newbie to RxJava and I do my learning through some samples. I have done a sample RxJava Retrofit app which will show the details of movies from http://www.omdbapi.com. The app has a searchBox using which the user inputs a movie name, which will be fetched and sent as api request, and upon the response, the result is shown. My issue is whenever an error occur, after the onError, the edittext observable doesn't emit anything anymore. So, basically if one movie search fails due to any API error, I need to close and re-launch the app in order to continue with the movie search. How can I observe to edittext changes even after the onError? Below is my code:
public class MainActivity extends AppCompatActivity implements SearchView.OnQueryTextListener {
private static final String TAG = "LOG";
#Inject
ApiInterface mApiInterface;
private MainActivityViewHelper mMainActivityViewHelper;
BehaviorSubject<String> mStringSubject = BehaviorSubject.create();
private ViewModel mVm;
private Observer<MovieData> mMovieDataObserver;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//setting up views / databining and dagger
mVm = new ViewModel();
ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
binding.setVm(mVm);
App.get(this).getAppComponent().inject(this);
mMainActivityViewHelper = new MainActivityViewHelper();
mMainActivityViewHelper.setSearchToolbar(this, this);
searchSubscription().subscribe(mMovieDataObserver);
}
private Observable<MovieData> searchSubscription() {
mMovieDataObserver = new Observer<MovieData>() {
#Override
public void onSubscribe(#NonNull Disposable d) {
Log.d(TAG, "onSubscribe: ");
}
#Override
public void onNext(#NonNull MovieData movieData) {
Log.d(TAG, "onNext: " + movieData);
mVm.loading.set(false);
mVm.moviedata.set(movieData);
}
#Override
public void onError(#NonNull Throwable e) {
Log.d(TAG, "onError: " + e.getMessage());
searchSubscription();
}
#Override
public void onComplete() {
Log.d(TAG, "onComplete: ");
}
};
Observable<MovieData> movieDataObservable = mStringSubject
.filter(s -> s != null)
.doOnNext(s -> Log.d(TAG, s))
.debounce(500, TimeUnit.MILLISECONDS)
.doOnNext(s -> Log.d(TAG, "onCreate: " + s))
.flatMap(s -> mApiInterface.getMovie(s))
.onErrorReturn(throwable -> null)
.doOnSubscribe(disposable -> mVm.loading.set(true))
.doFinally(() -> mVm.loading.set(false))
.subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread());
return movieDataObservable;
}
#Override
public boolean onCreateOptionsMenu(final Menu menu) {
getMenuInflater().inflate(R.menu.menu_home, menu);
return true;
}
#Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_search:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
Anim.circleReveal(this, R.id.searchtoolbar, 1, true, true);
else
mMainActivityViewHelper.mSearchToolbar.setVisibility(View.VISIBLE);
mMainActivityViewHelper.mItem.expandActionView();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
#Override
public boolean onQueryTextSubmit(String query) {
return true;
}
#Override
public boolean onQueryTextChange(String newText) {
mStringSubject.onNext(newText);
return true;
}
}
Here , when an error occurs in the retrofit observable, the error continues to your main observable, and then your stream stops. You can skip the error from the retrofit observable to pass to your main stream. You could make use of any
error handling operators as specified here How to ignore error and continue infinite stream?
Try to apply some error handling to your retrofit observable , which you are returning from the flatMap
For example
Observable<MovieData> movieDataObservable = mStringSubject
.filter(s -> s != null)
.doOnNext(s -> Log.d(TAG, s))
.debounce(500, TimeUnit.MILLISECONDS)
.doOnNext(s -> Log.d(TAG, "onCreate: " + s))
.flatMap(s -> mApiInterface.getMovie(s).onErrorReturn(throwable -> new MovieData()))
.doOnSubscribe(disposable -> mVm.loading.set(true))
.doFinally(() -> mVm.loading.set(false))
.subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread());
Here , on error , the retrofit observable will return an empty MovieData object instead of calling onError. You could check for an empty MovieData object for error case and handle accordingly
I have the following methods
Document createDocument(String url);
List<MediaContent> getVideo(Document doc);
List<MediaContent> getImages(Document doc);
List< MediaContent> will be consumed by
void appendToRv(List<MediaContent> media);
I like to use RxJava2 such that
CreateDocument -> getVideo ->
-> appendToRv
-> getImages ->
(also, the video output should be ordered before images).
How would I go about doing that? I tried flatMap, but it seems to only allow a single method to be used
Single<List<MediaContent>> single =
Single.fromCallable(() -> createDocument(url))
// . ?? ..
// this is the part i am lost with
// how do i feed document to -> getVideo() and getImage()
// and then merge them back into the subscriber
//
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
single.subscribe(parseImageSubscription);
The DisposableSingleObserver
parseImageSubscription = new DisposableSingleObserver<List<MediaContent>>() {
#Override
public void onSuccess(List<MediaContent> media) {
if(media!=null) {
appendToRv(media);
}
}
#Override
public void onError(Throwable error) {
doSnackBar("error loading: '" + q + "'");
}
};
the single observables for getVideos and getImages
Single<List<MediaContent>> SingleGetImage(Document document ) {
return Single.create(e -> {
List<MediaContent> result = getImage(document);
if (result != null) {
e.onSuccess(result);
}else {
e.onError(new Exception("No images found"));
}
});
}
Single<List<MediaContent>> singleGetVideo(Document document ) {
return Single.create(e -> {
List<MediaContent> result = getVideo( document);
if (result != null) {
e.onSuccess(result);
}else {
e.onError(new Exception("No videos found"));
}
});
}
assuming you want to execute in parallel the getVideos and getImages requests, you can use flatMap() with zip(), zip will collect the 2 emissions from both Singles, and you can combine the 2 results to a new value, meaning you can sort the videos MediaContent list , and combine it with the images MediaContent list, and return unified list (or whatever other object you'd like):
Single<List<MediaContent>> single =
Single.fromCallable(() -> createDocument(url))
.flatMap(document -> Single.zip(singleGetVideo(document), SingleGetImage(document),
(videoMediaContents, imageMediaContents) -> //here you'll have the 2 results
//you can sort combine etc. and return unified object
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
single.subscribe(parseImageSubscription)
Observable.zip() could implement it perfect. The Observer will receive a merged result by this method.
public void zip() {
Observable<Integer> observable1 = Observable.just(1);
Observable<Integer> observable2 = Observable.just(2);
Observable.zip(observable1, observable2, new Func2<Integer, Integer, Integer>() {
#Override
public Integer call(Integer integer, Integer integer2) {
return integer + integer2;
}
}).subscribe(new Observer<Integer>() {
#Override
public void onCompleted() {
}
#Override
public void onError(Throwable e) {
}
#Override
public void onNext(Integer o) {
Logger.i(o.toString());
//Here will print 3.
}
});
}
My application must do two things in general:
Accept only one network request at the same time
Retry if request failed
That's how I implement it:
public class RequestsLocker {
private volatile boolean isLocked;
public <T> Observable.Transformer<T, T> applyLocker() {
if(!isLocked()) {
return observable -> observable
.doOnSubscribe(() -> {
lockChannel();
})
.doOnUnsubscribe(() -> {
freeChannel();
});
} else {
return observable -> Observable.error(new ChannelBusyException("Channel is busy now."));
}
}
private void lockChannel() {
isLocked = true;
}
private void freeChannel() {
isLocked = false;
}
public boolean isLocked() {
return isLocked;
}
}
Looks nice.
Now my retryWhen implementation:
public static Observable<?> retryWhenAnyIoExceptionWithDelay(Observable<? extends Throwable> observable) {
return observable.flatMap(error -> {
// For IOExceptions, we retry
if (error instanceof IOException) {
return Observable.timer(2, TimeUnit.SECONDS);
}
// For anything else, don't retry
return Observable.error(error);
});
}
There is how I use it:
public Observable<List<QueueCarItem>> finishService(int id, PaymentType paymentType, String notes) {
return carsQueueApi.finishService(id, new FinishCarServiceRequest(paymentType.getName(), notes))
.compose(requestsLocker.applyLocker(RequestsLocker.RequestChannel.CHANGE));
}
...
public void finishCarService(QueueCarItem carItem, PaymentType paymentType,
String notes, Subscriber<List<QueueCarItem>> subscriber) {
queueApiMediator.finishService(carItem.getId(), paymentType, notes)
.subscribeOn(ioScheduler)
.observeOn(uiScheduler)
.doOnError(this::handleError)
.retryWhen(RxOperatorsHelpers::retryWhenAnyIoExceptionWithDelay)
.subscribe(subscriber);
}
The main problem that doOnUnsubscribe() called on any error and then locker is open for any new request until the timer expires and resubscribing happens again. That's the problem. While the timer is ticking user can make another request.
How I can fix it?
The problem is that you're applying your transformer to the source observable i.e. before your retrywhen.
When there is an error you're always going to unsubscribe from and then resubscribe to the source observable
leading to your doOnUnsubscribe being called.
I suggest you try
public Observable<List<QueueCarItem>> finishService(int id, PaymentType paymentType, String notes) {
return carsQueueApi.finishService(id, new FinishCarServiceRequest(paymentType.getName(), notes));
}
public void finishCarService(QueueCarItem carItem, PaymentType paymentType,
String notes, Subscriber<List<QueueCarItem>> subscriber) {
queueApiMediator.finishService(carItem.getId(), paymentType, notes)
.subscribeOn(ioScheduler)
.observeOn(uiScheduler)
.doOnError(this::handleError)
.retryWhen(RxOperatorsHelpers::retryWhenAnyIoExceptionWithDelay)
.compose(requestsLocker.applyLocker(RequestsLocker.RequestChannel.CHANGE));
.subscribe(subscriber);
}
PS: The apply locker transformer looks a bit different i.e. it doesn't take an argument in the code you linked.
Using retryWhen, to avoid unsubscribe onError you must use onErrorResumeNext which wont unsubscribe you.
Take a look of this example
/**
* Here we can see how onErrorResumeNext works and emit an item in case that an error occur in the pipeline and an exception is propagated
*/
#Test
public void observableOnErrorResumeNext() {
Subscription subscription = Observable.just(null)
.map(Object::toString)
.doOnError(failure -> System.out.println("Error:" + failure.getCause()))
.retryWhen(errors -> errors.doOnNext(o -> count++)
.flatMap(t -> count > 3 ? Observable.error(t) : Observable.just(null)),
Schedulers.newThread())
.onErrorResumeNext(t -> {
System.out.println("Error after all retries:" + t.getCause());
return Observable.just("I save the world for extinction!");
})
.subscribe(s -> System.out.println(s));
new TestSubscriber((Observer) subscription).awaitTerminalEvent(500, TimeUnit.MILLISECONDS);
}
Also about concurrency, if you do an operation in a flatMap operator, you can specify the Max concurrent.
public final <R> Observable<R> flatMap(Func1<? super T, ? extends Observable<? extends R>> func, int maxConcurrent) {
if (getClass() == ScalarSynchronousObservable.class) {
return ((ScalarSynchronousObservable<T>)this).scalarFlatMap(func);
}
return merge(map(func), maxConcurrent);
}
You can see more examples here https://github.com/politrons/reactive
My current solution is not to unlock RequestLocker on IoException as in this case request will be repeated after delay.
public <T> Observable.Transformer<T, T> applyLocker() {
if(!isLocked()) {
return observable -> observable.doOnSubscribe(() -> {
lockChannel();
}).doOnNext(obj -> {
freeChannel();
}).doOnError(throwable -> {
if(throwable instanceof IOException) {
return; // as any request will be repeated in case of IOException
}
freeChannel(channel);
});
} else {
return observable -> Observable.error(new ChannelBusyException("Channel is busy now"));
}
}