Android Retrofit parallel file download issue - android

I have an usecase in my application for downloading two zip files from the server. For that i have been using retrofit+rxjava(created two seperate retrofit services). For parallel I execution have been subscribing both retrofit service in new thread and later combining it using zip operator. It's working fine. But later I added map operator to both service for unzipping operation but it not executing the code written in the map operator and the control is passing directly to zip operation. I don't how to tackle this and i am newbie in reactive world.
What I have been tried so far
Observable<Response<ResponseBody>> dFileObservable = dbDownloadApi.downloadDealerData(WebServiceConstants.ACTION_DEALER_DATA,
params.getDealerNumber(),params.getUserId(),params.getClientId(), params.getSessionId()).subscribeOn(Schedulers.newThread());
dFileObservable.map(new Function<Response<ResponseBody>, String>() {
#Override
public String apply(Response<ResponseBody> responseBody) throws Exception {
String header = responseBody.headers().get("Content-Disposition");
String filename = header.replace("attachment; filename=", "");
String downloadFolderPath = fileManager.makeAndGetDownloadFolderPath();
String dealerZipPath = fileManager.makeFolder(downloadFolderPath, StrConstants.DEALER_FOLDER_NAME);
fileManager.writeDownloadedFileToDisk(dealerZipPath,filename, responseBody.body().source());
String dealerFilePath = dealerZipPath+File.separator+filename;
unzipUtility.unzip(dealerFilePath, fileManager.makeAndGetDownloadFolderPath()+File.separator+ StrConstants.GENERAL_FOLDER_NAME);
return dealerFilePath;
}
});
Observable<Response<ResponseBody>> generalFileObservable = dbDownloadApi.downloadGeneralData(WebServiceConstants.ACTION_GENERAL_DATA,
params.getDealerNumber(),params.getUserId(),params.getClientId(), params.getSessionId()).subscribeOn(Schedulers.newThread());;
generalFileObservable.map(new Function<Response<ResponseBody>, String>() {
#Override
public String apply(Response<ResponseBody> responseBody) throws Exception {
String header = responseBody.headers().get("Content-Disposition");
String filename = header.replace("attachment; filename=", "");
String downloadFolderPath = fileManager.makeAndGetDownloadFolderPath();
String generalZipPath = fileManager.makeFolder(downloadFolderPath, StrConstants.GENERAL_FOLDER_NAME);
fileManager.writeDownloadedFileToDisk(generalZipPath,filename, responseBody.body().source());
String generalFilePath = generalZipPath+File.separator+filename;
unzipUtility.unzip(generalFilePath, fileManager.makeAndGetDownloadFolderPath()+File.separator+ StrConstants.GENERAL_FOLDER_NAME);
return generalFilePath;
}
});
Observable<String> zipped = Observable.zip(dealerFileObservable, generalFileObservable, new BiFunction<Response<ResponseBody>, Response<ResponseBody>, String>() {
#Override
public String apply(Response<ResponseBody> responseBodyResponse, Response<ResponseBody> responseBodyResponse2) throws Exception {
System.out.println("zipped yess");
return null;
}
}).observeOn(Schedulers.io());
zipped.subscribe(getObserver());
and the getObserver() function
private Observer<String> getObserver(){
return new Observer<String>() {
#Override
public void onSubscribe(Disposable d) {
}
#Override
public void onNext(String value) {
System.out.println("------------total time-----------");
System.out.println("result value-->"+value);
}
#Override
public void onError(Throwable e) {
}
#Override
public void onComplete() {
}
};
}
When the code executes the control is transferred to the apply() function in zip operator and the map operator in both observable is not getting executed.
And there is another question
I am merging/zipping the two observables and the type passed to the operator is Response<"ResponseBody">. Actually i need the downloaded file path there(string type)and for that what should i do?
**
Updated the Solution as described by #Yaroslav Stavnichiy and now its working
**
Observable<String> deObservable = dbDownloadApi.downloaddData(WebServiceConstants.ACTION_DATA,
params.getNumber(),params.getId(),params.getCtId(), params.getSessionId())
.flatMap(new Function<Response<ResponseBody>, ObservableSource<String>>() {
#Override
public ObservableSource<String> apply(Response<ResponseBody> responseBody) throws Exception {
String zipPath = fileManager.processDownloadedFile(StrConstants.FOLDER_NAME,
StrConstants.FILE_NAME,responseBody.body().source());
return Observable.just(zipPath);
}
}).map(new Function<String, String>() {
#Override
public String apply(String filePath) throws Exception {
String unzipDestinationPath = fileManager.makeAndGetDownloadFolderPath()+
File.separator+ StrConstants.FOLDER_NAME;
unzipUtility.unzip(filePath, unzipDestinationPath);
return unzipDestinationPath;
}
}).subscribeOn(Schedulers.newThread());

What you are effectively doing is:
Observable a = ...;
Observable b = ...;
a.map(...);
b.map(...);
Observable.zip(a, b).subscribe(f);
map() (as well as all other rx-operators) does not mutate the source. It returns new observable that you can use in further computations. In your code you are ignoring those returned objects. You are zipping original observables, not the mapped ones, that's why mapper functions do not get invoked.
I think you wanted to do the following:
Observable a = ... .map(...);
Observable b = ... .map(...);
Observable.zip(a, b).subscribe(f);

Related

RxJava thread not waiting for result

I have this method that I am trying to pull data from an API, and then update the text view. Everything works except getRecipeName doesn't finish after the "end Method" log. .getRecipeName() uses RetroFit to pull from an API.
I am currently learning MVP, Dagger, RxJava, and Butterknife all at once using
Mindork's Github page on MVP Architecture
I commented out the .subscribeOn and .observeOn to see the result difference and nothing changed.
#Override
public void onRandomButtonClicked() {
getMvpView().showLoading();
Log.e(TAG, "Random Method Open");
getCompositeDisposable().add(getDataManager()
.getRecipeName()
//.subscribeOn(getSchedulerProvider().io())
//.observeOn(getSchedulerProvider().ui())
.subscribe(new Consumer<String>() {
#Override
public void accept(String s) throws Exception {
Log.e(TAG, "accept");
getMvpView().updateTextView(title);
}
}));
Log.e(TAG, "end method");
}
Here is my getRecipeName() method
#Override
public Observable<String> getRecipeName() {
/*Create handle for the RetrofitInstance interface*/
GetDataService service = RetrofitClientInstance.getRetrofitInstance().create(GetDataService.class);
Call<RecipeList> call = service.getRecipe();
call.enqueue(new Callback<RecipeList>() {
#Override
public void onResponse(#NonNull Call<RecipeList> call, #NonNull retrofit2.Response<RecipeList> response) {
Log.e("onResponse","Recipe is Successful = " + response.isSuccessful());
//if response is false then skip to avoid null object reference
if (response.isSuccessful()) {
RecipeList drinkRecipe = response.body();
List<Recipe> recipes = drinkRecipe.getDrinks();
jokeText = String.valueOf(recipes.size());
Recipe myRecipe = recipes.get(0);
jokeText = myRecipe.getStrDrink();
Log.e("On Response", "Result2: " + jokeText);
}
//jokeText = "null";
}
#Override
public void onFailure(Call<RecipeList> call, Throwable t) {
Log.e("On Response","Failure");
}
});
//return jokeText;
return Observable.fromCallable(new Callable<String>() {
#Override
public String call() throws Exception {
return jokeText;
}
});
}
Solution
So as the comments stated RxJava Adapter was the correct way to go. I will just post my working code on myself using the adapter. I found it very difficult to find a working example.
//single api call using retrofit and rxjava
#SuppressLint("CheckResult")
private void getRandomButtonClick(){
retrofit = RetrofitClientInstance.getRetrofitInstance();
retrofit.create(GetDataService.class).getRecipe()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::handleResults, this::handleError );
}
private void handleResults(RecipeList recipeList) {
int i = recipeList.getDrinks().size();
Log.e(TAG, "size is: "+ i);
Recipe recipe = recipeList.getDrinks().get(0);
getMvpView().updateTextView(recipe.getStrDrink());
}
private void handleError(Throwable t){
Log.e("Observer", "");
}
My Retrofit Client Instance
public static Retrofit getRetrofitInstance() {
if (retrofit == null) {
retrofit = new retrofit2.Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build();
}
return retrofit;
}
My Interface
public interface GetDataService {
//#Headers({})
#GET("random.php")
Observable<RecipeList> getRecipe();
I found a great resource to reference for me to correctly implement this. Retrofit Android
The reason is because your observable is returning jokeText every time it is subscribed upon. It returns immediately after invocation and will not wait for your network operation.
One possible solution is to use the RxJavaCallAdapter. Link here: https://github.com/square/retrofit/tree/master/retrofit-adapters/rxjava2
It will automatically convert your API returns to observables. No need to manually invoke retrofit requests. Just process the response and convert it to your desired object from there.
Another approach would be to wrap your entire sequence in an Observable.create or Observable.fromAsync.

How to wait for retrofit response in rxjava

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()

RxJava Loop for doing async work

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());
}
});
}

How can I create a stream in RxJava2 (Android) that has no input value, but produces a String?

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";
}

Connecting RxJava Observables in layers

I have 3 layers in my app. Layer1 subscribes to Observable from layer2. Layer2 subscribes to layer3 in order to emit returned data to layer1.
Layer1
layer2.getData(data).subscribe(newData -> {Log.d("onNext", "returned");},
throwable -> {Log.d("onError", throwable.getMessage());});
Suppose layer3 has a method called downloadDataFromApi(data);
public Observable<Data> getData(String data) {
return Observable.create(new Observable.OnSubscribe<Data>() {
#Override
public void call(Subscriber<? super Data> subscriber) {
Data data = new Data();
subscriber.onNext(data);
subscriber.onCompleted();
// Can't find a way to connect to layer3.
}
});
}
What do I need to do in layer2's getData() method? I basically want to have logics before returning Observable back to layer1.
Does that make sense?
Just return the Observable directly. Then layer1 handles subscription as usual.
class Layer2 {
public Observable<Data> getData(String data) {
return layer3.getData(data);
}
}
From what I see you have 3 layers (presentation, business logic, data access).
So what you could do is the following:
class PresentationLayer {
private BusinessLogicLayer layer;
PresentationLayer() {
layer = new BusinessLogicLayer();
}
public void showName() {
layer.getNameWithoutRxPrefix()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<String>() {
#Override
public void accept(String name) throws Exception {
// show name somewhere
Log.d("PresentationLayer", "name: " + name);
}
});
}
}
class BusinessLogicLayer {
private DataAccessLayer layer;
BusinessLogicLayer() {
layer = new DataAccessLayer();
}
public Observable<String> getNameWithoutRxPrefix() {
return layer.getName()
.map(new Function<String, String>() {
#Override
public String apply(String name) throws Exception {
return name.replace("Rx", "");
}
});
}
}
class DataAccessLayer {
public Observable<String> getName() {
return Observable.just("RxAndroid");
}
}
As you can see, I return an Observable in my data access layer (getName), and chain another method to it in my business logic method (map) before returning it to the presentation layer.

Categories

Resources