Background:
I'm trying to load an image with Glide and rxjava2 (to make use of background thread).
I'm still confused how each component of rxjava2 api works.
So below are my questions.
Q1: Is it impossible to add Single to CompositeDisposable?
Whenever I try to add Single into CompositeDisposable, Android Studio warns me that there is type mismatch.
However if I call toObservable() before subscribeOn, error message goes away.
Q2: Correct my usage of rxjava2 with Glide if there are some problems.
private void loadImages(final String imageUri) {
Single<GlideRequest<Drawable>> glideSingle = Single.fromCallable(
new Callable<GlideRequest<Drawable>>() {
#Override
public GlideRequest<Drawable> call() {
return GlideApp.with(MainActivity.this).load(imageUri);
}
}
);
// mSubscription is just new CompositeDisposable()
mSubscription.add(
glideSingle
.toObservable() // I have to call this to suppress error message
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeWith(
new DisposableObserver<GlideRequest<Drawable>>() {
#Override
public void onNext(GlideRequest<Drawable> drawableGlideRequest) {
RequestOptions backgroundTransformOptions = new RequestOptions()
.transforms(
new CenterCrop(),
new BlurTransformation(60),
new ColorFilterTransformation(
ContextCompat.getColor(MainActivity.this, R.color.colorBackgroundOverlay))
);
drawableGlideRequest
.into(mAlbumArt);
drawableGlideRequest
.apply(backgroundTransformOptions)
.into(mMainBackground);
}
#Override
public void onError(Throwable e) {
}
#Override
public void onComplete() {
}
}
)
);
}
Rather then subscribeWith you should be able to use the regular subscribe method.
This will return a Disposable, e.g. without lambda support this would look something like this:
mSubscription.add(
glideSingle
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<GlideRequest<Drawable>>() {
#Override
public void accept(GlideRequest<Drawable> drawableGlideRequest) throws Exception {
RequestOptions backgroundTransformOptions = new RequestOptions()
.transforms(
new CenterCrop(),
new BlurTransformation(60),
new ColorFilterTransformation(
ContextCompat.getColor(MainActivity.this, R.color.colorBackgroundOverlay))
);
drawableGlideRequest
.into(mAlbumArt);
drawableGlideRequest
.apply(backgroundTransformOptions)
.into(mMainBackground);
}
}, new Consumer<Throwable>() {
#Override
public void accept(Throwable throwable) throws Exception {
}
})
);
Related
I tried to loop an Api call based on an Array size, and put the result into an adapter. But I get null when I tried to use the results of the api call because it is outside a subscribe. how do I use the variable outside the subscribe?
Here's my Code
private void getMoviesData(final String lang) {
favouriteMovies = databaseHelper.getAllFavouriteMovies();
for (int i = 0; i < favouriteMovies.size(); i++) {
api.getSingleMovie(favouriteMovies.get(i).getMovieId(),apiKey, lang)
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.subscribe(new Observer<SingleMovieResponse>() {
#Override
public void onCompleted() {
}
#Override
public void onError(Throwable e) {
progressBar.setVisibility(View.GONE);
Log.d(TAG, "Show Data Error!: " + e);
}
#Override
public void onNext(SingleMovieResponse singleMovieResponse) {
for (int i = 0; i < singleMovieResponse.getSpoken_languages().size(); i++ ) {
MoviesObject moviesObject = new MoviesObject();
moviesObject.setMovieId(singleMovieResponse.getId());
moviesObject.setTitle(singleMovieResponse.getTitle());
moviesObject.setPosterPath(singleMovieResponse.getPoster_path());
moviesObject.setOverview(singleMovieResponse.getOverview());
moviesObject.setReleaseDate(singleMovieResponse.getRelease_date());
moviesObject.setVoteAverage(singleMovieResponse.getVote_average());
if (singleMovieResponse.getOverview().equals("")) {
moviesObject.setOverview(Objects.requireNonNull(getActivity()).getResources().getString(R.string.no_overview_movie));
}
movies.add(moviesObject);
}
}
});
}
recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
MovieAdapter movieAdapter = new MovieAdapter(movies,getActivity());
recyclerView.setAdapter(movieAdapter);
recyclerView.setHasFixedSize(true);
progressBar.setVisibility(View.GONE);
recyclerView.setVisibility(View.VISIBLE);
}
how to get movies values outside the subscribe?
You don't. The result from the stream is asynchronous, which means that it will be called some time later, while you setting the adapter with data is synchronous. Also, don't use a loop when handling streams, use the Reactive operators as shown below. I can't compile to test whether it works, but below is roughly the correct answer to your question.
private void getMoviesData(final String lang) {
favouriteMovies = databaseHelper.getAllFavouriteMovies();
Observable.fromIterable(favouriteMovies) // Creates a stream of Favourite Movies
.flatMap(new Function<FavouriteMovie, ObservableSource<?>>() { // Makes a request for each FavouriteMovie
#Override
public ObservableSource<?> apply(FavouriteMovie favouriteMovie) throws Exception {
return api.getSingleMovie(favouriteMovie.getMovieId(), apiKey, lang);
}
})
.map(new Function<SingleMovieResponse, MoviesObject>() { // Maps the response to the wanted object
#Override
public MoviesObject apply(SingleMovieResponse singleMovieResponse) throws Exception {
return map(singleMovieResponse);
}
})
.toList() // Merges each item of the Observable stream into a Single stream as List<Object>
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.subscribe(new SingleObserver<List<MoviesObject>>() {
#Override
public void onSubscribe(Disposable d) {
}
#Override
public void onSuccess(List<MoviesObject> moviesObjects) {
progressBar.setVisibility(View.GONE);
recyclerView.setVisibility(View.VISIBLE);
movieAdapter.setList(moviesObjects); // Create this method in the adapter to update list
movieAdapter.notifyDataSetChanged();
}
#Override
public void onError(Throwable e) {
progressBar.setVisibility(View.GONE);
Log.d(TAG, "Show Data Error!: " + e);
}
});
movieAdapter = new MovieAdapter(movies,getActivity()); // MovieAdapter should be a class field
recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
recyclerView.setHasFixedSize(true);
recyclerView.setAdapter(movieAdapter);
}
private MoviesObject map(SingleMovieResponse singleMovieResponse) {
MoviesObject moviesObject = new MoviesObject();
moviesObject.setMovieId(singleMovieResponse.getId());
moviesObject.setTitle(singleMovieResponse.getTitle());
moviesObject.setPosterPath(singleMovieResponse.getPoster_path());
moviesObject.setOverview(singleMovieResponse.getOverview());
moviesObject.setReleaseDate(singleMovieResponse.getRelease_date());
moviesObject.setVoteAverage(singleMovieResponse.getVote_average());
if (singleMovieResponse.getOverview().equals("")) {
moviesObject.setOverview(Objects.requireNonNull(getActivity()).getResources().getString(R.string.no_overview_movie));
}
return moviesObject;
}
P.S. Try introducing lambda expressions to make your life easier, or even better Kotlin!!!
I use retrofit2 with rxjava extension.
I have a list of REST API urls and want to do this:
for each
check whether a corresponding file locally exists
if yes: call the API and store the response or the HTTP error
if not: store a customized error
return the list of those results
My problem is: apply returns (with an empty RequestResult) before the server response is received. I think, I understand why, but I don't know how to fix it, because I need to return a RequestResult and not the Retrofit observable.
How can this be solved?
Here is my code:
#GET
Observable<Response<ResponseBody>> enroll(#Url String url);
class RequestResult {
CustomException error;
Response<ResponseBody> response;
}
Observable<ClassOfListItem> observable = Observable.fromIterable(listOfItems);
observable
.flatMap(new Function<ClassOfListItem, ObservableSource<RequestResult>>() {
#Override
public ObservableSource<RequestResult> apply(ClassOfListItem listItem) throws Exception {
RequestResult requestResult = new RequestResult();
if (fileExists(listItem.url)) {
Observable<Response<ResponseBody>> callObservable = restAPI.enroll(listItem.url)
.subscribeOn(Schedulers.io());
callObservable
.subscribe(new DisposableObserver<Response<ResponseBody>>() {
#Override
public void onNext(Response<ResponseBody> responseBodyResponse) {
onPremiseEnrollmentResult.response = responseBodyResponse;
}
#Override
public void onError(Throwable e) {
onPremiseEnrollmentResult.error = new CustomException(e);
}
#Override
public void onComplete() {
}
});
}
else {
requestResult.error = new CustomException("file not found");
}
return Observable.just(requestResult);
}
}
.toList()
.observerOn(AndroidScheduler.mainThread())
.subscribe(new DisposableSingleObserver<List<RequestResult>>() {
#Override
public void onError(Throwable e) {
Log.d("onError", e.getMessage());
}
#Override
public void onSuccess(List<RequestResult> requestResults) {
// parse results
}
}
)
The flatMap() operator allows you to turn one observable into a different observable. You have a nested observer chain inside your apply() which is not part of the observer chain, so it will be empty because it has not completed yet.
To fix this, when the file exists, return the observable.
observable
.flatMap(new Function<ClassOfListItem, ObservableSource<RequestResult>>() {
#Override
public ObservableSource<RequestResult> apply(ClassOfListItem listItem) throws Exception {
RequestResult requestResult = new RequestResult();
if (fileExists(listItem.url)) {
return restAPI.enroll(listItem.url)
.subscribeOn(Schedulers.io());
}
return Observable.error( new CustomException("file not found") );
}
}
.toList()
.observerOn(AndroidScheduler.mainThread())
.subscribe(new DisposableSingleObserver<List<RequestResult>>() {
#Override
public void onError(Throwable e) {
Log.d("onError", e.getMessage());
}
#Override
public void onSuccess(List<RequestResult> requestResults) {
// parse results
}
}
If you need to capture both errors and successes into the list, then you can add map() operator to wrap RequestResult around the response and onErrorResumeNext() to wrap RequestResult around the error before the toList() operator.
If you are making api call on background thread then what you can do is invoke it synchronously....in your case your retrofit api method would change to following
Call<Response<ResponseBody>> enroll(#Url String url);
and you'd invoke by calling restAPI.enroll(listItem.url).execute()
I had 2 tables TimeStamps and Infraction, I want to do
something like that using retrofit with Rx Android :
Request-> I get TimeStamps (if it's changed)
-> I send new request to get Infractions
else I display infractions from database
this is what I did using Retrofit, is that correct ??
Observable<TimeStamps> callTimeStamp = apiInterface.getTimeStamp();
TimeStamps stamps = realm.where(TimeStamps.class).findFirst();
callTimeStamp.flatMap(new Function<TimeStamps, ObservableSource<List<Infraction>>>() {
#Override
public ObservableSource<List<Infraction>> apply(TimeStamps timeStamps) throws Exception {
if(!timeStamps.getInfractionTimeStamps().equalsIgnoreCase( stamps.getInfractionTimeStamps()))
return apiInterface.getInfractions();
else
return null;
}
}).subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread()).subscribe(new Observer<List<Infraction>>() {
#Override
public void onSubscribe(Disposable d) {
}
#Override
public void onNext(List<Infraction> infractions) {
}
#Override
public void onError(Throwable e) {
}
#Override
public void onComplete() {
}
});
No
TimeStamps stamps = realm.where(TimeStamps.class).findFirst();
This obtains stamps on the current thread
if(!timeStamps.getInfractionTimeStamps().equalsIgnoreCase( stamps.getInfractionTimeStamps()))
This attempts to access that stamps instance on a different thread, so you'll get an IllegalStateException
return null;
Even if it did work, this line would make RxJava2 throw a NullPointerException
.subscribeOn(Schedulers.newThread())
This could easily be Schedulers.io() instead so that it wouldn't create too many threads (although then of course you should make sure you use try(Realm realm = ...) or finally { realm.close() })
.subscribe(new Observer>() {
This is wrong unless you "properly implement onSubscribe" which is not expected at all, this should be new DisposableObserver<List<Infraction>>().
In which case your Retrofit interface should probably expose Single<T>, as singles automatically unsubscribe when done.
Single<TimeStamps> callTimeStamp = apiInterface.getTimeStamp();
callTimeStamp.flatMap((timeStamps) -> {
try(Realm realm = Realm.getDefaultInstance()) {
TimeStamps stamps = realm.where(TimeStamps.class).findFirst();
if(!timeStamps.getInfractionTimeStamps().equalsIgnoreCase( stamps.getInfractionTimeStamps()))
return apiInterface.getInfractions();
else
return Single.never();
}
}).subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new DisposableObserver<List<Infraction>>() {
#Override
public void onNext(List<Infraction> infractions) {
}
#Override
public void onError(Throwable e) {
}
#Override
public void onComplete() {
}
});
I am using Compressor library, i want compress the images using RxJava. Following is the example from the library documentation.
new Compressor(this)
.compressToFileAsFlowable(actualImage)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<File>() {
#Override
public void accept(File file) {
compressedImage = file;
}
}, new Consumer<Throwable>() {
#Override
public void accept(Throwable throwable) {
throwable.printStackTrace();
showError(throwable.getMessage());
}
});
This works very well. Now i want to compress a list of images, how can i use this technique to get a list of compressed file paths?
I tried adding this method in a for loop but the returned list was empty because the accept method was not even called once and the code reached the return statement. Following is my method
#NonNull
private ArrayList<String> compressFiles(ArrayList<String> files, File directory) {
final ArrayList<String> filesToReturn = new ArrayList<>();
for (final String filepath : files) {
new Compressor(this)
.compressToFileAsFlowable(new File(filepath))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<File>() {
#Override
public void accept(File file) {
filesToReturn.add(file.getAbsolutePath());
}
}, new Consumer<Throwable>() {
#Override
public void accept(Throwable throwable) {
throwable.printStackTrace();
}
});
}
return filesToReturn;
}
How can i change this method using RxJava so accept method only triggers when all the files have been compressed and list is filled?
I tried searching for RxJava loops/Flatmap but i couldn't figure them out. I am new to RxJava. Any help or pointers would be highly appreciated.
In RxJava, you would use Observable.fromIterable():
Observable.fromIterable(files)
.flatMapIterable(files -> files)
.flatMap(filepath -> new Compressor(this).compressToFileAsFlowable(new File(filepath)))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<File>() {
#Override
public void accept(File file) {
compressedImage = file;
}
}, new Consumer<Throwable>() {
#Override
public void accept(Throwable throwable) {
throwable.printStackTrace();
showError(throwable.getMessage());
}
});
Here are two more possible results:
Completable will be called when all images have been processed
Flowable will push results to subscriber as they are finished processing (honors backpressure)
Please look at subscribeOn. I use the computation scheduler, because cpu bound work should be executed on a bounded threadpool. IO is unbounded.
When you subscribe to Completeable or Flowable you have to apply observeOn with Android-MainUI-Scheduler.
Rember that subscribing to an Flowable/Observable should only be done, when state muste be changed. Otherwise compose observables together.
// Will call complete, when all files finish
private Completable compressFiles(ArrayList<String> files) {
return Flowable.fromIterable(files)
.subscribeOn(Schedulers.io())
.map(new Function<String, File>() {
#Override
public File apply(String s) throws Exception {
return Paths.get(s).toFile();
}
})
.flatMapCompletable(new Function<File, CompletableSource>() {
#Override
public CompletableSource apply(File file) throws Exception {
return new Compressor(appContext)
.compressToFileAsFlowable(file)
.singleOrError()
.toCompletable();
}
});
}
// Will push each file to subscriber as one finishes
private Flowable<File> compressFiles2(ArrayList<String> files) {
return Flowable.fromIterable(files)
.subscribeOn(Schedulers.io())
.map(new Function<String, File>() {
#Override
public File apply(String s) throws Exception {
return Paths.get(s).toFile();
}
})
.flatMap(new Function<File, Publisher<? extends File>>() {
#Override
public Publisher<? extends File> apply(File file) throws Exception {
return new Compressor(appContext)
.compressToFileAsFlowable(file)
.subscribeOn(Schedulers.computation());
}
});
}
In Android, I want to use the call AdvertisingIdClient.getAdvertisingIdInfo(getContext()).getId() on a separate thread (IO-thread) and handle the string on the main thread.
I wan't to do this with RxJava2.
This is what I have now: (which works)
SingleOnSubscribe<String> source = new SingleOnSubscribe<String>() {
#Override
public void subscribe(SingleEmitter<String> e) throws Exception {
e.onSuccess(AdvertisingIdClient.getAdvertisingIdInfo(getContext()).getId());
}
};
Single.create(source)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnError(new Consumer<Throwable>() {
#Override
public void accept(Throwable throwable) throws Exception {
Timber.e(throwable.getMessage());
}
})
.subscribe(new Consumer<String>() {
#Override
public void accept(String s) throws Exception {
advertisingId = s;
}
});
What I would prefer, which is purely taste, is if I could "just" create the stream and handle it all in the flow of methods. Such as: (warning, super pseudo code)
Completable
.empty()
.produce(() -> String {
return makeString();
})
.sub/obs-On()...
.subscribe(coolString -> {mVariable = coolString})
So, Make an Observable and turn it into an Observable by executing some function.
Just use defer or fromCallable like in this example:
Observable<String> stringObservable = Observable.fromCallable(() -> {
return getStuff();
});
Test
#Test
public void fromCallable() throws Exception {
Observable<String> stringObservable = Observable.fromCallable(() -> {
return getStuff();
});
ExecutorService executorService = Executors.newSingleThreadExecutor(runnable -> {
return new Thread(runnable, "myFancyThread");
});
Scheduler scheduler = Schedulers.from(executorService);
TestObserver<String> test = stringObservable.subscribeOn(scheduler)
.test();
test.await()
.assertResult("wurst");
assertThat(test.lastThread().getName()).contains("myFancyThread");
}
private String getStuff() {
return "wurst";
}