I have a requirement where I want to make multiple GET requests, what will be best practice in java not using RxJava.
Here I have given parameter as i in getPhotos(), specifies id which loads data in json accordingly. This can run concurrently.
PhotoList list = UnplashClient.getUnplashClient().create(PhotoList.class);
for(int i=0; i<10; i++) {
call = list.getPhotos(i);
call.enqueue(new Callback<List<Photo>>() {
#Override
public void onResponse(Call<List<Photo>> call, Response<List<Photo>> response) {
}
#Override
public void onFailure(Call<List<Photo>> call, Throwable t) {
}
});
If you are looking for serial execution of api calls one after the other, you can make use of Task. This is similar to what Rx java is doing.
Please find the pseudo code below with an example :
private void fetchPhotos() {
Task<Photo> task = null;
List<Photo> photos = new ArrayList<>();
for (int i = 0; i < 10; i++) {
if (task == null) {
task = getPhoto(i);
} else {
final int pos = i;
task = task.onSuccessTask(photo -> {
photos.add(photo);
return getPhoto(pos);
});
}
}
task.addOnCompleteListener((photoTask) -> {
photos.add(photoTask.getResult()); //Adding the final result.
for (int i = 0; i < photos.size(); i++) {
Log.i("DEMO", photos.get(i).toString());
}
});
}
private Task<Photo> getPhoto(int i) {
Task<Photo> task = Tasks.call(Executors.newSingleThreadExecutor() /*You can specify the threading here*/, () -> new Photo(i) /*Your logic to fetch photo goes here...*/);
return task;
}
class Photo {
int pos = 0;
Photo(int p) {
this.pos = p;
}
#Override
public String toString() {
return String.valueOf(pos);
}
}
Running above code, you can see result in sequential order printed in Logcat. Here the chaining of requests happens from the success of previous request.
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 an array list which is initialized by using retrofit 2 method , and i want to use that array list on another method, how to do that ?
i have tried to initiate array variable outside the retrofit 2 method and then recall it + initiate its length in retrofit 2 method and also trying to use static variable nothing works
array = yAxisData
code retrofit 2 method
public void getIPList(String UserId){
Call<IpListResponse> getIPList = mApiService.getIPList(
UserId
);
getIPList.enqueue(new Callback<IpListResponse>() {
#Override
public void onResponse(Call<IpListResponse> call, Response<IpListResponse> response) {
if (response.isSuccessful()) {
List<IpList> list = new ArrayList<>();
list = response.body().getIpList();
yAxisData = new double[list.size()];
for (int i =0;i<list.size();i++) {
yAxisData[i] = Double.valueOf(list.get(i).getIp());
}
Log.d("TESTER", String.valueOf(yAxisData.length));
}
}
#Override
public void onFailure(Call<IpListResponse> call, Throwable t) {
}
});
}
method where i want to use array
public void setGrafik1(){
String[] axisData = {" ","I", "II", "III", "IV", "V", "VI", "VII", "VIII"};
List yAxisValues = new ArrayList();
List axisValues = new ArrayList();
Line line = new Line(yAxisValues);
for(int i = 0; i < yAxisData.length; i++){
axisValues.add(i, new AxisValue(i).setLabel(axisData[i]));
}
for (int i = 0; i < yAxisData.length; i++){
yAxisValues.add(new PointValue(i, (float) yAxisData[i]));
}
.....
.....
}
I have an API call that returns list of items by page. I use retrofit to implement and the interface is:
Observable<QueryResult> queryData(#Body QueryParams params);
The QueryParams and QueryResult is define as:
class QueryParams {
int pageIndex, pageSize; // for pagination;
... // other query criteria
}
class QueryResult {
int pageIndex, pageSize;
int totalCount; // This is the total data size which is used to know if there are still data to retreat.
... // List of data returned by page;
}
And I use this code to get the first page of 100 data item:
params.pageIndex = 1;
params.pageSize = 100;
queryData(params).subscribe(...);
The API is designed as to get the data list page by page so I could efficiently response to the UI representation.
Somehow, in some cases, I need to get all the data at once and process with some tasks before representing to UI. With the interface designed like this, I have to call the queryData() several times till all the data fetched or at least twice (the first one to get the totalCount and pass it to pageSize for the second call).
So, my question is how do I do it with RxJava manners chaining API calls to get all the data?
Thanks in advance.
Update A solution from #Abu
Observable<QueryResult> query(final QueryParams params) {
return queryData(params)
.concatMap(new Func1<QueryResult, Observable<QueryResult>>() {
#Override
public Observable<QueryResult> call(final QueryResult result) {
int retrievedCount = result.getPageSize() * (result.getPageIndex() - 1) + result.resultList.size();
if (retrievedCount >= result.getCount()) {
return Observable.just(result);
}
QueryParams nextParams = params.clone();
nextParams.setPageIndex(results.getPageIndex() + 1);
return query(nextParams).map(new Func1<QueryResult, QueryResult>() {
#Override
public QueryResult call(QueryResult nextResult) {
nextResult.resultList.addAll(result.resultList);
return nextResult;
}
});
}
}
One may be to do it recursively with concatMap and concatWith operator.
Here is a sample code.
private Observable<List<Integer>> getResponse(final int index) {
return getData(index)
.concatMap(new Function<List<Integer>, ObservableSource<? extends List<Integer>>>() {
#Override
public ObservableSource<? extends List<Integer>> apply(List<Integer> integers) throws Exception {
if (index == 10) {
return Observable.just(integers);
}else {
return Observable.just(integers)
.concatWith(getResponse(index + 1));
}
}
});
}
private Observable<List<Integer>> getData(int index){
List<Integer> dataList = new ArrayList<>();
for (int i = 0; i < 10; i++) {
dataList.add(index*10 + i);
}
return Observable.just(dataList);
}
Usage:
getResponse(1)
.subscribeOn(Schedulers.io())
.subscribe(new Consumer<List<Integer>>() {
#Override
public void accept(List<Integer> integers) throws Exception {
Log.i(TAG, "Data: " + Arrays.toString(integers.toArray()));
}
}, new Consumer<Throwable>() {
#Override
public void accept(Throwable throwable) throws Exception {
Log.e(TAG, throwable.getMessage());
}
});
This will give you all data recursively in a order. You will get data for first index 1 , them index 2 , .......
If there is a better solution i am waiting to see it.
Edit:
To get complete list of data use can update you code this way:
private Observable<List<Integer>> getResponse(final int index) {
return getData(index)
.concatMap(new Function<List<Integer>, ObservableSource<? extends List<Integer>>>() {
#Override
public ObservableSource<? extends List<Integer>> apply(final List<Integer> integerList) throws Exception {
if (index < 9){
return getResponse(index+1)
.map(new Function<List<Integer>, List<Integer>>() {
#Override
public List<Integer> apply(List<Integer> integers) throws Exception {
integers.addAll(integerList);
return integers;
}
});
}else {
return Observable.just(integerList);
}
}
});
}
private Observable<List<Integer>> getData(int index){
Util.printThreadInfo(index);
final List<Integer> dataList = new ArrayList<>();
for (int i = 0; i < 10; i++) {
dataList.add(index*10 + i);
}
return Observable.just(dataList);
}
Usage:
Observable.defer(new Callable<ObservableSource<? extends List<Integer>>>() {
#Override
public ObservableSource<? extends List<Integer>> call() throws Exception {
return getResponse(1);
}
}).subscribeOn(Schedulers.io())
.subscribe(new Consumer<List<Integer>>() {
#Override
public void accept(List<Integer> integers) throws Exception {
Collections.sort(integers);
Log.i(TAG, "Data: " + Arrays.toString(integers.toArray()));
}
}, new Consumer<Throwable>() {
#Override
public void accept(Throwable throwable) throws Exception {
Log.e(TAG, throwable.getMessage());
}
});
This will give you complete data at once.
I think you shouldn't get all data this way because if your page size is 100 you are crating 100 network call. You api should give you all data for a single call.
I just update my answer to show how this can be done.
I'm implementing a two-level nested recyclerView and both recycler views make an API call using retrofit. This is the method that makes the synchronous request:
public void loadSectionStories(String sessionKey, CuratedSection section) {
Call<JsonArray> subCall;
subCall = TravelersApi.endpoint().getCuratedSectionTopics(sessionKey, section.id);
try {
Response<JsonArray> response = subCall.execute();
if(response.code() != 200) {
Toast.makeText(getApplicationContext(), "Cannot load page as of the moment.", Toast.LENGTH_SHORT).show();
return;
}
JsonArray rawStories = response.body();
if(rawStories.size() == 0) {
//TODO: show placeholder
return;
}
ArrayList<CuratedSectionItem> stories = new ArrayList<>();
for(int i = 0; i < rawStories.size(); i++) {
JsonObject jStories = rawStories.get(i).getAsJsonObject();
JSONObject temp = new JSONObject(jStories.toString());
JsonObject author = jStories.get("author").getAsJsonObject();
CuratedSectionItem story = new CuratedSectionItem();
story.title = jStories.get("title").getAsString();
story.avatar = author.get("profile_photo").getAsString();
story.displayPhoto = temp.getString("primary_photo");
story.username = author.get("username").getAsString();
story.description = jStories.get("content").getAsString();
story.topicId = jStories.get("id").getAsString();
story.postId = jStories.get("first_post_id").getAsString();
story.hasReacted = false;
story.upvotes = jStories.get("stats").getAsJsonObject().get("upvotes").getAsInt();
stories.add(story);
}
section.stories = stories;
} catch (IOException e) {
Log.d("ERROR!", e.toString());
e.printStackTrace();
} catch (JSONException e) {
e.printStackTrace();
}
}
This is the method that makes the asynchronous request and also calls loadSectionStories in a thread:
public void loadCuratedSections(final int start, final int limit) {
SharedPreferences prefs = getSharedPreferences("user_session", MODE_PRIVATE);
final String sessionKey = prefs.getString("session_key", null);
Call<JsonArray> call;
call = TravelersApi.endpoint().getCuratedSections(sessionKey);
call.enqueue(new Callback<JsonArray>() {
#Override
public void onResponse(Call<JsonArray> call, Response<JsonArray> response) {
if(response.code() != 200) {
Toast.makeText(getApplicationContext(), "Cannot load page as of the moment.", Toast.LENGTH_SHORT).show();
return;
}
JsonArray rawSections = response.body();
if(rawSections.size() == 0) {
return;
}
for (int i = start; i < limit; i++) {
JsonObject jSection = rawSections.get(i).getAsJsonObject();
final CuratedSection section = new CuratedSection();
section.id = jSection.get("id").getAsString();
section.header = jSection.get("section_header").getAsString();
section.isShown = jSection.get("is_shown").getAsBoolean();
Thread thread = new Thread(new Runnable() {
#Override
public void run() {
loadSectionStories(sessionKey, section);
}
});
thread.start();
curatedSections.add(section);
}
}
#Override
public void onFailure(Call<JsonArray> call, Throwable t) {
Log.d("ERROR!", t.toString());
t.printStackTrace();
}
});
}
Everything is working fine except the fact that section.stories returns null. It doesn't make sense to me because of this statement section.stories = stories inside loadSectionStories.
If you are using section.stories before your synchronous request is completed (which is running in new threads) then it will return null which is currently happening.
so either you have to remove new thread flow if you want to use it after your first asynchronous request is completed,
or you have to reload your recycler view when you stories is updated.
Also why are you executing your synchronous request(loadSectionStories) in new thread, is it not similar to asynchronous request?
Retrofit asyncRetrofit = new Retrofit.Builder()
.baseUrl(URLS.MAIN_SERVER_URL)
// below line create thread for syncrouns request
.callbackExecutor(Executors.newSingleThreadExecutor())
.addConverterFactory(GsonConverterFactory.create())
.client(httpClient.build())
.build();
this will run your request in asyncronous
I'm running a for loop and saving data on firebase inside it.
Here's my code:
private int counter = 1;
pProgress.setMessage("Posting...");
pProgress.show();
pProgress.setCancelable(false);
for (int i=0; i<imageArray.size(); i++) {
Uri file = Uri.fromFile(new File(imageArray.get(i).toString()));
String key = database.getReference().push().getKey();
UploadTask uploadTask = storage.getReference().child(AccessToken.getCurrentAccessToken().getUserId()).child(key).child("product_image").putFile(file);
uploadTask.addOnFailureListener(new OnFailureListener() {
#Override
public void onFailure(#NonNull Exception exception) {
// Handle unsuccessful uploads
}
}).addOnSuccessListener(new OnSuccessListener<UploadTask.TaskSnapshot>() {
#Override
public void onSuccess(UploadTask.TaskSnapshot taskSnapshot) {
counter++;
if (counter == imageArray.size()) {
Toast.makeText(getBaseContext(), "successfully posted!", Toast.LENGTH_SHORT).show();
imageView.setImageResource(0);
pDescription.setText("");
pDuration.setText("");
pPrice.setText("");
label.setText("Label");
if (pProgress.isShowing()) {
pProgress.dismiss();
}
}
// taskSnapshot.getMetadata() contains file metadata such as size, content-type, and download URL.
#SuppressWarnings("VisibleForTests")
Uri downloadUrl = taskSnapshot.getDownloadUrl();
imageUrls.add(downloadUrl.toString());
database.getReference().child(AccessToken.getCurrentAccessToken().getUserId()).child(id).child("pDescription").setValue(pDescription.getText().toString());
database.getReference().child(AccessToken.getCurrentAccessToken().getUserId()).child(id).child("pDuration").setValue(pDuration.getText().toString());
database.getReference().child(AccessToken.getCurrentAccessToken().getUserId()).child(id).child("pPrice").setValue(pPrice.getText().toString());
if (imageUrls != null) {
for (int i=0; i<imageUrls.size(); i++) {
Log.d("imageUrlsSize", String.valueOf(imageUrls.size()));
String idtwo = database.getReference().push().getKey();
database.getReference().child(AccessToken.getCurrentAccessToken().getUserId()).child(id).child(idtwo).child("imageUrl").setValue(downloadUrl.toString());
if (imageUrls.size() > 0) {
imageUrls.clear();
}
}
}
}
});
}
I want to show a ProgressDialog and dismiss it when the data is uploaded successfully. Similarly, I want to clear the EditText fields and ImageView too.
How can I figure out imageArray has ended so that I can dismiss ProgressDialog and clear edittext as well as imageview?
Please let me know.
One way would be to check the count in your callbacks. You would check it in onFailure and onSuccess. Something like this
for (int i = 0; i < imageArray.size(); i++) {
final int currentCount = i + 1;
/** code **/
onSuccess() {
if (currentCount == imageArray.size()) // do something
}
}
You may also need to declare imageArray as final. This has the disadvantage that you will know that the last element has been processed, but you will have no idea if there are other request still pending. A better approach would be to use a member field in your class e.g.
class YourClass{ private int currentCount = 0; }
and you would again count this in your callbacks e.g.
for (int i = 0; i < imageArray.size(); i++) {
onSuccess() {
currentCount++;
if (currentCount == imageArray.size()) // do something
}
}
Beeing a Real-time database there is no cetain moment in which we can say that we have loaded all the reconars from the database or that an ArrayList has reached it's last element. This happening because there is a possibility that in every moment something can change, i.e. an item/items can be added or deleted. This is the way in which Firebase was built.
But a short answer to your question, first move the declaration of your imageUrls ArrayList inside the onSuccess() method otherwise will be null due the asynchronous behaviour of this method. Second, define a variable in your class named count and increase the value of it each time onSuccess() method is called and compare it with the size of your list like this:
int count = 0;
onSuccess() {
count++;
if(count == imageArray.size() : do somethig ? do something else)
}
You are handling async tasks so you need to encapsulate your job and work with a Queue and callbacks. Try something like that:
class Job{
private Runnable completeCallback;
private String key;
private Uri file;
public Job(String key, Uri file) {
this.key = key;
this.file = file;
}
public void setCompleteCallback(Runnable completeCallback) {
this.completeCallback = completeCallback;
}
void doWork(){
UploadTask uploadTask = storage.getReference().child(AccessToken.getCurrentAccessToken().getUserId()).child(key).child("product_image").putFile(file);
uploadTask.addOnFailureListener(new OnFailureListener() {
#Override
public void onFailure(#NonNull Exception exception) {
// Handle unsuccessful uploads
completeCallback.run();
}
}).addOnSuccessListener(new OnSuccessListener<UploadTask.TaskSnapshot>() {
#Override
public void onSuccess(UploadTask.TaskSnapshot taskSnapshot) {
// taskSnapshot.getMetadata() contains file metadata such as size, content-type, and download URL.
#SuppressWarnings("VisibleForTests")
Uri downloadUrl = taskSnapshot.getDownloadUrl();
imageUrls.add(downloadUrl.toString());
database.getReference().child(AccessToken.getCurrentAccessToken().getUserId()).child(id).child("pDescription").setValue(pDescription.getText().toString());
database.getReference().child(AccessToken.getCurrentAccessToken().getUserId()).child(id).child("pDuration").setValue(pDuration.getText().toString());
database.getReference().child(AccessToken.getCurrentAccessToken().getUserId()).child(id).child("pPrice").setValue(pPrice.getText().toString());
if (imageUrls != null) {
for (int i=0; i<imageUrls.size(); i++) {
Log.d("imageUrlsSize", String.valueOf(imageUrls.size()));
String idtwo = database.getReference().push().getKey();
database.getReference().child(AccessToken.getCurrentAccessToken().getUserId()).child(id).child(idtwo).child("imageUrl").setValue(downloadUrl.toString());
if (imageUrls.size() > 0) {
imageUrls.clear();
}
}
}
completeCallback.run();
}
});
}
}
class Queue {
ArrayList<Job> jobs = new ArrayList<>();
private int total;
public void execute(){
total = imageArray.size();
for (int i=0; i<imageArray.size(); i++) {
Uri file = Uri.fromFile(new File(imageArray.get(i).toString()));
String key = database.getReference().push().getKey();
Job job = new Job(key, file);
job.setCompleteCallback(new Runnable() {
#Override
public void run() {
jobs.remove(job);
updateUi();
}
});
jobs.add(job);
}
for (int i = jobs.size() - 1; i >= 0; i--) {
Job job = jobs.get(i);
job.doWork();
}
}
private void updateUi() {
Log.d("UI", "Completed " + jobs.size() + "/" + total);
}
}
I figured it out with the help of Murat K's answer.
I just shifted this code
database.getReference().child(AccessToken.getCurrentAccessToken().getUserId()).child(id).child("pDescription").setValue(pDescription.getText().toString());
database.getReference().child(AccessToken.getCurrentAccessToken().getUserId()).child(id).child("pDuration").setValue(pDuration.getText().toString());
database.getReference().child(AccessToken.getCurrentAccessToken().getUserId()).child(id).child("pPrice").setValue(pPrice.getText().toString());
out of the for loop and now everything is working as expected.