RecyclerView doesn't update the UI after data changed - android

I am new in android development, after make a network request trying to update item view in the RecyclerView control without scrolling it. As far as I understand items gets refreshed during scroll via onBindViewHolder event.here is the code. Using notifyItemChanged method to update UI but it doesn't work until user scroll.
Note : favoriteMatches is not my data source. It is another list of objects which stores user favorites,
inside onBindViewHolder event I am cheking if item is favoriteMatches.contains(match) then render as fav item.
call.enqueue(new Callback<AddRemoveFavoriteRequest.Response>() {
#Override
public void onResponse(Call<AddRemoveFavoriteRequest.Response> call, Response<AddRemoveFavoriteRequest.Response> response) {
AddRemoveFavoriteRequest.Response body = response.body();
Utilities.dismissProgressDialog(getActivity(),progressBar);
if(body.error == null){
if(add){
favoriteMatches.add(matchId);
}
else {
favoriteMatches.remove(matchId);
}
adapter.notifyItemChanged(absolutePosition);
Preferences.getDefaultPreferences().edit()
.putStringSet(Preferences.PREF_FAVORITES,favoriteMatches)
.apply();
}else{
Utilities.showSnackBar(getActivity(),recyclerView,body.error);
}
}
#Override
public void onFailure(Call<AddRemoveFavoriteRequest.Response> call, Throwable t) {
t.printStackTrace();
Utilities.dismissProgressDialog(getActivity(),progressBar);
}
});

notifyItemChanged takes the position at which the item changed.
You are adding and removing elements, so that position is going to move around. Instead, you can individually notify at positions.
int changedPosition = 0;
if(add){
changedPosition = favoritesMatches.size();
favoriteMatches.add(matchId);
adapter.notifyItemInserted(changedPosition);
}
else {
changedPosition = favoriteMatches.indexOf(matchId); // might not work
favoriteMatches.remove(matchId);
adapter.notifyItemRemoved(changedPosition);
}
Or, instead update the entire list
adapter.notifyDataSetChanged();
Read more about notifying the adapter, but note
a RecyclerView adapter should not rely on notifyDataSetChanged() since the more granular actions should be used.

Related

Using Observable.from and flatMap to perform operation on each item in an List and to set the result in each list element

I get a List of searchItems. For each element in this list exists a Lat und Lng. For these coordinates I use a googleService that takes these two values and returns a JsonObject with the name (city name) for this location. This works fine! In my onNext I can see in my log output the city.
Now my problem: I want to store this name in the corresponding list element. like: item.setLocation(loc) -> but I can not set access to the item in onNext() ! how can I get access to item ??
Observable.from(searchItems)
.flatMap(item -> googleService.getCityNameFromLatLngObserable(item.getLat(), item.getLng(), null)
.subscribeOn(Schedulers.io()))
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Subscriber<JsonObject>() {
#Override
public void onCompleted() {
view.updateSearches(searchItem);
}
#Override
public void onError(Throwable e) {
}
#Override
public void onNext(JsonObject response) {
if (response.get("results").getAsJsonArray().size() > 0) {
String loc = response.get("results").getAsJsonArray()
.get(0).getAsJsonObject().get("address_components").getAsJsonArray()
.get(2).getAsJsonObject().get("long_name").toString();
Log.d("CONAN", "City: "+loc);
item.setLocation(loc); //does not work
}
}
});
Actually you shouldn't really change your searchItems but rather create new ones with the new info. This is to enforce immutability, which is very dear to Rx.
There is more than one way to do this, but here's one possible way. You need the item to call your Google service so you cannot use zip or any of its friends. So let's keep the first bit like you have it and get an observable emitting each search item:
Observable.from(searchItems)
Now let's go on and create new search items with their location.
Observable.from(searchItems)
.flatMap(item -> googleService
.getCityNameFromLatLngObserable(
item.getLat(),
item.getLng(), null)
.map(jsonObject -> /* create a
new item with location */))
.subscribeOn(Schedulers.io()))
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Subscriber<JsonObject>() {
#Override
public void onCompleted() {
view.updateSearches(searchItem);
}
#Override
public void onError(Throwable e) {
}
#Override
public void onNext(Item item) {
// Your items with location
}
});
What I did was map the results of the googleService. getCityNameFromLatLngObserable to an item that has the location from the result. Notice how here you have access to both the item and the json object return by google?
Now inside the map function you can either use a function such as createWithLocation(item, getLocation(jsonObject)) or use something like the builder pattern:
item.toBuilder()
.location(getLocation(jsonObject))
.build();
Notice also that on the subscriber onNext you have now an Item instead of a JsonObject and with the location filled. I assume you can code getLocation since your example already has this.
One last thing, you can also call the setter in the item instance and not create a new one. I would advise against this because you'll be adding a side effect to a call that others might not expect. It's usually a source for bugs. However, you can still do it :)
Hope it helps

Android Realm Update Asynchronously Using RxJava

I have this query to update data already in my realm table;
for (MyGameEntrySquad squad : response.body().getSquad()) {
subscription = realm.where(RealmPlayer.class).equalTo("id", squad.getPlayer().getId())
.findFirstAsync()
.asObservable()
.subscribe(new Action1<RealmObject>() {
#Override
public void call(RealmObject realmObject) {
}
});
}
I would like to perform this query asynchronously then display the results on the UI.
Basically, whatever is been returned by response.body().getSquad() has an id matching a record already in the DB; and that is what am using in my equalTo method.
Based on the data received, I would like to update two columns on each of the record matching the IDs.
However, I am facing a few challenges on this:
The Action1 in subscribe is returning a RealmObject instead of a PlayerObject
How to proceed from here
Any guidance on this will be appreciated.
Thanks
Update
if (response.isSuccessful()) {
//asynchronously update the existing players records with my squad i.e is_selected
for (MyGameEntrySquad squad : response.body().getSquad()) {
realm.where(RealmPlayer.class).equalTo("id", squad.getPlayer().getId())
.findFirstAsync()
.<RealmPlayer>asObservable()
.filter(realmPlayer -> realmPlayer.isLoaded())
.subscribe(player -> {
realm.beginTransaction();
if (squad.getPlayer().getPosition().equals("GK")) {
player.setPlaygroundPosition("gk");
player.setIsSelected(true);
}
// pick the flex player
if (squad.isFlex()) {
player.setPlaygroundPosition("flex");
player.setIsSelected(true);
}
// pick the Goalie
if (squad.getPlayer().getPosition().equals("GK")) {
player.setPlaygroundPosition("gk");
player.setIsSelected(true);
}
// pick the DFs
if ((squad.getPlayer().getPosition().equals("DF")) && (!squad.isFlex())) {
int dfCounter = 1;
player.setPlaygroundPosition(String.format(Locale.ENGLISH, "df%d", dfCounter));
player.setIsSelected(true);
dfCounter++;
}
// pick the MFs
if ((squad.getPlayer().getPosition().equals("MF")) && (!squad.isFlex())) {
int mfCounter = 1;
player.setPlaygroundPosition(String.format(Locale.ENGLISH, "mf%d", mfCounter));
player.setIsSelected(true);
mfCounter++;
}
// pick the FWs
if ((squad.getPlayer().getPosition().equals("FW")) && (!squad.isFlex())) {
int fwCounter = 1;
player.setPlaygroundPosition(String.format(Locale.ENGLISH, "mf%d", fwCounter));
player.setIsSelected(true);
fwCounter++;
}
realm.copyToRealmOrUpdate(player);
realm.commitTransaction();
updateFieldPlayers();
});
}
hideProgressBar();
}
realm.where(RealmPlayer.class).equalTo("id", squad.getPlayer().getId())
.findFirstAsync()
.<RealmPlayer>asObservable()
.subscribe(new Action1<RealmPlayer>() {
#Override
public void call(RealmPlayer player) {
}
});
You should do like that.
Btw, it's bad idea to do it in a cycle - check in method of RealmQuery.
for (MyGameEntrySquad squad : response.body().getSquad()) { // btw why is this not `Observable.from()`?
subscription = realm.where(RealmPlayer.class).equalTo("id", squad.getPlayer().getId())
.findFirstAsync()
.asObservable()
This should not be on the UI thread. It should be on a background thread. On a background thread, you need to use synchronous query instead of async query.
Even on the UI thread, you'd still need to filter(RealmObject::isLoaded) because it's an asynchronous query, and in case of findFirstAsync() you need to filter for RealmObject::isValid as well.
For this case, you would not need asObservable() - this method is for observing a particular item and adding a RealmChangeListener to it. Considering this should be on a background thread with synchronous query, this would not be needed (non-looper background threads cannot be observed with RealmChangeListeners).
You should also unsubscribe from any subscription you create when necessary.
And yes, to obtain RealmPlayer in asObservable(), use .<RealmPlayer>asObservable().
In short, you should put that logic on a background thread, and listen for changes on the UI thread. Background thread logic must be done with the synchronous API. You will not need findFirstAsync for this.

Why async methods call sooner in Retrofit 2?

As you can see in the code below, I've call the getPostByPageId() method to get data from server and then check if the data was returned back, I do other jobs.
private void recyclerViewJobs() {
getPostByPageId();
if (pageDtoList.size() > 0) {
emptyText.setVisibility(View.GONE);
recyclerView.setVisibility(View.VISIBLE);
PagesAdapter adapter = new PagesAdapter(pageDtoList, context);
recyclerView.setAdapter(adapter);
recyclerView.setLayoutManager(new LinearLayoutManager(context));
} else {
emptyText.setVisibility(View.VISIBLE);
recyclerView.setVisibility(View.GONE);
}
}
.....
private void getPostByPageId() {
IPageEndPoint pageEndPoint = ServiceGenerator.createService(IPageEndPoint.class);
Call<List<PageDto>> call = pageEndPoint.getPostByPageId(profileId);
call.enqueue(new retrofit2.Callback<List<PageDto>>() {
#Override
public void onResponse(Call<List<PageDto>> call, Response<List<PageDto>> response) {
if (response.isSuccessful()) {
pageDtoList = response.body();
} else {
log.toast("response is not successful for getPostByPageId");
}
}
#Override
public void onFailure(Call<List<PageDto>> call, Throwable t) {
log.toast("getPostByPageId onFailure");
}
});
}
I don't know why in the recyclerViewJobs() method, the if condition work first?
maybe I could not explain my issue very well but my big problem is to
know when I want to get some data from REST and then use this data to
another REST service, how can I do this job because enqueue in
retrofit works asynchronous and do not wait for first method, because
of that it is beginning the second method in another thread and
because my data was not gotten from first method yet, my program was
crashed and I didn't get the answer. That is my problem.....
Cheers
That's because retrofit's enqueue method is not a blocking method. which means the Thread calling it won't wait for it to finish its job.
Like #Amir said, If you have other things to do after calling getPostByPageId(), you should put them after retrofit's callback:
private void recyclerViewJobs() {
getPostByPageId();
}
private void getPostByPageId() {
IPageEndPoint pageEndPoint = ServiceGenerator.createService(IPageEndPoint.class);
Call<List<PageDto>> call = pageEndPoint.getPostByPageId(profileId);
call.enqueue(new retrofit2.Callback<List<PageDto>>() {
#Override
public void onResponse(Call<List<PageDto>> call, Response<List<PageDto>> response) {
if (response.isSuccessful()) {
pageDtoList = response.body();
if (pageDtoList.size() > 0) {
emptyText.setVisibility(View.GONE);
recyclerView.setVisibility(View.VISIBLE);
PagesAdapter adapter = new PagesAdapter(pageDtoList, context);
recyclerView.setAdapter(adapter);
recyclerView.setLayoutManager(new LinearLayoutManager(context));
} else {
emptyText.setVisibility(View.VISIBLE);
recyclerView.setVisibility(View.GONE);
}
} else {
log.toast("response is not successful for getPostByPageId");
}
}
#Override
public void onFailure(Call<List<PageDto>> call, Throwable t) {
log.toast("getPostByPageId onFailure");
}
});
}
Your async call run first But your if-condition always goes false because if-condition not behave as you expected! As soon as your async request sent it goes to check if-condition and because your list is empty (your response from server is not fetch yet) it always return false.
If you want to call next operation just after your WebService response was successful you can do it with following code:
public void onResponse(Call<List<PageDto>> call, Response<List<PageDto>> response) {
if (response.body().YOUR_LIST.size() > 0) {
emptyText.setVisibility(View.GONE);
recyclerView.setVisibility(View.VISIBLE);
PagesAdapter adapter = new PagesAdapter(pageDtoList, context);
recyclerView.setAdapter(adapter);
recyclerView.setLayoutManager(new LinearLayoutManager(context));
//CALL_YOUR_NEXT webservice here
} else {
emptyText.setVisibility(View.VISIBLE);
recyclerView.setVisibility(View.GONE);
}
}
In your processResponse do your job. But one thing you should consider is that response.isSuccessful() return true most of time and it doesn't mean your responseBody contains data. according to document:
isSuccessful() Returns true if code() is in the range [200..300).
So the better way is to check that your response list contains data or not.
But as a better way (a bit difficult if you are beginner) is using RxJava/RxAndroid. you can process your call just one after other with flatMap.
What is an asynchronous task? Not only for retrofit but the answer is same for all, when you use asynchronous task to write some code, it seems you allow that code to run asynchronously, that is, any time it wants to run and that too in background without disturbing any other tasks and not giving any kind of effect on main UI thread. So asynchronous task clearly defines itself as a task running in background and the code written above or below is never affected by it. So in your case it asynchronous task might not have completed or may be it is possible that it might not had started also but it does not disturb your if condition. https://developer.android.com/reference/android/os/AsyncTask.html

AutocompleteTextView is not updated dynamically

I am getting data (List) from an API and I am trying to update my AutcompleteTextView with this data.
This is how I currently do :
I have a TextWatcher which calls a the method to get the data in afterTextChanged, so every time the user stops typing the method is called, and the adapter is notified with ``notifyDataSetChanged :
//in onCreate
addressAdapter = new ArrayAdapter<String>(this,android.R.layout.simple_dropdown_item_1line,suggestions_address);
at_address.setAdapter(addressAdapter);
...
#Override
public void afterTextChanged(Editable s) {
autoComplete(s);
addressAdapter.notifyDataSetChanged();
//suggestions_address is the updated list, and when I print it I can see the
//results so it is not empty
Log.i("addresses",suggestions_address.toString());
}
...
class SuggestionQueryListener implements ResultListener<List<String>> {
#Override
public void onCompleted(List<String> data, ErrorCode error) {
if (error != ErrorCode.NONE) {
Toast.makeText(MainActivity2.this,error.toString(),Toast.LENGTH_LONG).show();
} else {
suggestions_address.clear();
for(int i = 0;i<data.size();i++){
suggestions_address.add(data.get(i));
}
}
}
}
public void autoComplete(CharSequence s) {
try {
String term = s.toString();
TextSuggestionRequest request = null;
request = new TextSuggestionRequest(term).setSearchCenter(new GeoCoordinate(48.844900, 2.395658));
request.execute(new SuggestionQueryListener());
if (request.execute(new SuggestionQueryListener()) != ErrorCode.NONE) {
//Handle request error
//...
}
} catch (IllegalArgumentException ex) {
//
}
}
But it seems that the adapter is not really updated because it doesn't show the suggestions when I type something.
Also, before doing this with an AutoCompleteTextView I did it with a listView, with the same process, and everything worked well.
Any ideas or solutions would be really appreciated
EDIT : I noticed something really strange : the data is not binded to the adapter, because adapter#getCount always returns 0, even if the list is not empty. But when I remove at_address.setAdapter(addressAdapter), the data adapter is updated and adapter#getCount returns the right number of elements.
I am really confused right now, please help !
Instead of this:
for(int i = 0;i<data.size();i++){
suggestions_address.add(data.get(i));
}
you can use just this:
suggestions_address.addAll(data);
you are calling notifyDataSetChanged after you start the request, you should call it after you get the result and update the suggestions_address, so call notifyDataSetChanged inside onCompleted

RecyclerView element update + async network call

I have a recyclerview which works as expected. I have a button in the layout that fills the list. The button is supposed to make a async call, and on result, I change the button's look. All this happens fine.
But when I click on the button and scroll down the list fast, the async call's result updates the new view's button(the view that is in place of the old one). How do I handle this? Can I have a handle on when a particular view gets reused?
Update :
Code piece of the adapter class that does the async call and the updation of ui.
#Override
public void onBindViewHolder(CommentsViewHolder holder, int position) {
try {
Comments comment = comments.get(position);
holder.bindView(comment,position);
}
catch(Exception ex){ex.printStackTrace();}
}
#Override
public int getItemCount() {
if(comments==null)
{return 0;}
return comments.size();
//return comments.length();
}
public class CommentsViewHolder extends RecyclerView.ViewHolder {
TextView score ;
TextView commentText;
TextView commentTime;
TextView avatarId;
ImageButton minusOne;
ImageButton plusOne;
ParseObject model;
public CommentsViewHolder(View itemView) {
super(itemView);
//itemView.setBackgroundColor(Color.DKGRAY);
minusOne =(ImageButton)itemView.findViewById(R.id.decScore);
plusOne =(ImageButton)itemView.findViewById(R.id.incScore);
commentText = (TextView)itemView.findViewById(R.id.comment);
score = (TextView)itemView.findViewById(R.id.commentScore);
commentTime =(TextView)itemView.findViewById(R.id.commentTime);
avatarId = (TextView)itemView.findViewById(R.id.ivUserAvatar);
}
public void bindView(Comments comment, int position) {
commentText.setText(comment.getCommentText());
score.setText(Integer.toString(comment.getScore()));
String timeText = DateUtils.getRelativeTimeSpanString( comment.getCreatedAt().getTime(), System.currentTimeMillis(), DateUtils.SECOND_IN_MILLIS).toString();
timeText = timeText.replace("hours","hrs");
timeText = timeText.replace("seconds","secs");
timeText = timeText.replace("minutes","mins");
commentTime.setText(timeText);
int commentHandler = comment.getCommenterHandle();
String commenterNumber = "";
if(commentHandler==0)
{
commenterNumber = "OP";
}
else{
commenterNumber = "#"+commentHandler;
}
avatarId.setText( commenterNumber);
model = comment;
String choice = "none";
minusOne.setEnabled(true);
plusOne.setEnabled(true);
minusOne.setVisibility(View.VISIBLE);
plusOne.setVisibility(View.VISIBLE);
for (ParseObject choiceIter : choices) {
if ((choiceIter.getParseObject("comment").getObjectId()).equals(comment.getObjectId())) {
choice = choiceIter.getString("userChoice");
break;
}
}
Log.i("debug",comment.getCommentText()+" "+comment.getScore()+" "+choice);
switch (choice) {
case "plusOne":
Log.i("darkplus","setting darkplus");
plusOne.setImageResource(R.drawable.ic_add_circle_black_18dp);
plusOne.setOnClickListener(reversePlusOneOnClickListener);
//minusOne.setOnClickListener(minusOneOnClickListener);
minusOne.setVisibility(View.GONE);
break;
case "minusOne":
Log.i("darkminus","setting darkminus");
minusOne.setImageResource(R.drawable.ic_remove_circle_black_18dp);
minusOne.setOnClickListener(reverseMinusOneOnClickListener);
//plusOne.setOnClickListener(plusOneOnClickListener);
plusOne.setVisibility(View.GONE);
break;
case "none":
Log.i("darkregular","setting regular");
minusOne.setImageResource(R.drawable.ic_remove_black_18dp);
plusOne.setImageResource(R.drawable.ic_add_black_18dp);
plusOne.setOnClickListener(plusOneOnClickListener);
minusOne.setOnClickListener(minusOneOnClickListener);
break;
}
}
View.OnClickListener reversePlusOneOnClickListener = new View.OnClickListener() {
#Override
public void onClick(View v) {
if (!FourUtils.isConnected(v.getContext())) {
return;
}
minusOne.setEnabled(false);
plusOne.setEnabled(false);
model.increment("plusOne", -1);
model.increment("score", -1);
model.saveEventually(new SaveCallback() {
#Override
public void done(ParseException e) {
if (e == null) {
ParseQuery<ParseObject> query = ParseQuery.getQuery("CommentChoice");
query.whereEqualTo("user", ParseUser.getCurrentUser());
query.whereEqualTo("comment", model);
query.fromPin(Four.COMMENT_CHOICE);
query.getFirstInBackground(new GetCallback<ParseObject>() {
#Override
public void done(ParseObject parseObject, ParseException e) {
if (e == null) {
if (parseObject == null) {
parseObject = ParseObject.create("CommentChoice");
parseObject.put("comment", model);
parseObject.put("user", ParseUser.getCurrentUser());
}
parseObject.put("userChoice", "none");
parseObject.pinInBackground(Four.COMMENT_CHOICE, new SaveCallback() {
#Override
public void done(ParseException e) {
if (e == null) {
score.setText(Integer.toString(model.getInt("score")));
//votes.setText((model.getInt("minusOne") + model.getInt("plusOne")) + " votes");
minusOne.setVisibility(View.VISIBLE);
plusOne.setImageResource(R.drawable.ic_add_black_18dp);
plusOne.setOnClickListener(plusOneOnClickListener);
minusOne.setEnabled(true);
plusOne.setEnabled(true);
// minusOne.setOnClickListener(minusOneOnClickListener);
BusProvider.getInstance().post(new NewCommentChoicesAdded());
} else {
e.printStackTrace();
}
}
});
}
else{e.printStackTrace();}
}
});
} else {
e.printStackTrace();
Log.i("plus1 error", e.getMessage());
}
}
});
}
};
When the async code is done, you should update the data, not the views. After updating the data, tell the adapter that the data changed. The RecyclerView gets note of this and re-renders your view.
When working with recycling views (ListView or RecyclerView), you cannot know what item a view is representing. In your case, that view gets recycled before the async work is done and is assigned to a different item of your data.
So never modify the view. Always modify the data and notify the adapter. bindView should be the place where you treat these cases.
Chet Haase from Google discusses your exact issue in this DevBytes video.
In short, the framework need to be notified that one of the Views is in "transient" state. Once notified, the framework will not recycle this View until its "transient" flag cleared.
In your case, before you execute the async action, call setHasTransientState(true) on the child View that should change when the async action completes. This View will not be recycled until you explicitly call setHasTransientState(false) on it.
Offtopic:
It looks like you might be manipulating UI elements from background threads. Don't do that! If you can have a reference to Activity then use its runOnUiThread(Runnable action) API instead. If getting a reference to Activity is difficult, you can obtain UI thread's Handler and use its post(Runnable action) API.
Without code to look at, this is going to be difficult (if not impossible) for people to provide an exact answer. However, based on this description it sounds as though your async network loading (using an AsyncTask or custom Loader?) is not specifically tied to an element being tracked by your adapter. You'll need to have some way of tying the two together since the child View objects shown by the RecyclerView are re-used to be more efficient. This also means that if a View is being reused and there is an active async operation tied to it, that async operation will need to be canceled. Otherwise you'll see what you see now: the wrong child View being updated with content from an older async call.

Categories

Resources