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.
Related
When a button is pressed, a dialog appears asking user for message, with the option of attaching an image (from url). The problem I'm having is once the recyclerview is filled with enough items to scroll, when the user scrolls quickly for some reason random images start popping up in seemingly random list items.
I know the problem has to come from when the image is actually placed into the imageview, since I can tell the link is added to the firebase db just fine.
When the image link is submitted, it's sent to /posts/$uid/$post-id in a HashMap. Kind of like this:
final Map<String, String> postMap = new HashMap<String, String>();
imagebutton.setOnClickListener((view) -> {
AlertDialog.Builder = new...
LayoutInflater in = ...
View dialogLayout = ...inflate(r.layout...., null);
build.setView(dialogLayout);
EditText imgText = ...
Button submit = ...
AlertDialog a = build.create();
submit.setOnClickListener((View) -> {
...
postMap.put("imgLink", imgText.getText().toString());
a.dismiss();
...
urlDialog.show();
Then a few more items are added to the map and pushed to firebase.
Firebase postRef = ref.child("posts").child(auth.getUid());
postMap.put("author", ...);
postMap.put("content", ...);
postRef.push().setValue(postMap);
But like I said, I'm almost 100% sure the problem is not in posting the information, just populating the recview
Here's my code for the list itself:
RecyclerView feed = (RecyclerView)findViewById(R.id.recycler);
if (ref.getAuth() != null) {
FirebaseRecyclerAdapter<TextPost, PostViewHolder> adapter = new FirebaseRecyclerAdapter<TextPost, PostViewHolder>(TextPost.class, R.layout.list_item, PostViewHolder.class, ref.child("posts").child(uid)) {
#Override
protected void populateViewHolder(final PostViewHolder postViewHolder, final TextPost textPost, int i) {
postViewHolder.content.setText(textPost.getContent());
postViewHolder.author.setText(textPost.getAuthor());
postViewHolder.score.setText(textPost.getScore());
postViewHolder.time.setText(textPost.getTime());
if (textPost.getImgLink() != null && !textPost.getImgLink().equals("")) {
Log.i(TAG, "Setting image");
new Thread(new Runnable() {
#Override
public void run() {
try {
final Bitmap pic = bitmapFromUrl(textPost.getImgLink());
postViewHolder.img.post(new Runnable() {
#Override
public void run() {
postViewHolder.img.setImageBitmap(pic);
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
...
feed.setAdapter(adapter);
I just started learning how to work with worker threads for network activities off the main UI thread so I assume I messed that up? I've gone through the logic over and over in my head and i can't seem to figure out what's going wrong here.
EDIT: I tried using AsyncTask instead of Threads and the problem persists. sos
All I had to do was set the ImageView drawable to null before populating the ImageView.
Like this:
#Override
protected void populateViewHolder(final ViewHolder v, final Object o, int i) {
//populate views
v.content.setText("...");
//Set imageview to null
v.imageview.setImageDrawable(null);*
if (o.getImageLink() != null && !o.getImageLink.equals("")) {
// Start AsyncTask to get image from link and populate imageview
new DownloadImageTask().execute(o.getImgLink(), v.imageview);
}
}
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
At the beginning of the chat app user see a list off groups (listview group) available and the user have the possibility to create a new group or click on some off the available groups and then start to write messages (listview messages). The functions CreateNewMessage and CreateNewGroup pushes information to firebase correctly
Above scenarios works finne problems arise when user navigates backwards (popBackStack()) from listview with messages to GroupFragment, here should user be presented a list off available groups but the listview is empty. The ReadGroupData() function is not reading the already created groups from firebase and inserts them in the group listview. How to make this happen?
GroupFragment:
public void ReadGroupData() {
Firebase firebaserootRef = new Firebase("https://000.firebaseio.com");
firebaserootRef.addChildEventListener(new ChildEventListener() {
#Override
public void onChildAdded(DataSnapshot snapshot, String s) {
if (snapshot.getValue() != null) {
Group newGroup = new Group((String)snapshot.child("name").getValue(),
(String) snapshot.child("id").getValue());
if(!groupKeyValues.contains(newGroup.GetId())) {
groupKeyValues.add(newGroup.GetId());
AddToLstViewGroup(newGroup);
System.out.println("Read group data from firebase and
inserted in listView");
}
}
}
});
}
public void AddToLstViewGroup(Group newGroup) {
groupNameList.add(newGroup);
if(groupAdapter == null) {
groupAdapter = new GroupAdapter(getActivity(), groupNameList);
}
if (lstViewGroup == null) {
lstViewGroup = (ListView) getView().
findViewById(R.id.listView_group);
}
lstViewGroup.setOnItemClickListener(onItemClickListener);
lstViewGroup.setOnItemLongClickListener(onItemLongClickListener);
groupAdapter.notifyDataSetChanged();
lstViewGroup.setAdapter(groupAdapter);
}
ChatFragment:
public void ReadChatMessages(Firebase firebaseRootRef) {
firebaseRootRef.addChildEventListener(new ChildEventListener() {
#Override
public void onChildAdded(DataSnapshot snapshot, String s) {
if (snapshot.child(GetGroupId()).child("messages").
getChildren() != null) {
for (DataSnapshot c :
snapshot.child(GetGroupId()).child("messages").getChildren()) {
String key = c.getKey();
Message newMessage = new Message();
newMessage.SetFrom((String) c.child("from").getValue());
newMessage.SetMsg((String)
c.child("message").getValue());
newMessage.SetTime((String) c.child("time").getValue());
newMessage.SetId((String) c.child("id").getValue());
if ((!msgKeyValues.contains(key)) ||
newMessage.GetFrom() != "") {
msgKeyValues.add(key);
AddToLstViewChat(newMessage);
//Automatic scrolls to last line in listView.
lstViewChat.setSelection(chatAdapter.getCount() -1);
}
}
}
}
public void AddToLstViewChat(Message newMessage) {
chatMsgList.add(newMessage);
if (chatAdapter == null) {
chatAdapter = new ChatAdapter(getActivity(), chatMsgList);
}
if(IsMsgFromMe(newMessage)) {
lstViewChat = (ListView)
getView().findViewById(R.id.listView_chat_message_me);
} else {
lstViewChat =
(ListView)getView().findViewById(R.id.listView_chat_message_others);
}
chatAdapter.notifyDataSetChanged();
lstViewChat.setAdapter(chatAdapter);
}
ChatActivity:
#Override
public void onBackPressed() {
if (getFragmentManager().getBackStackEntryCount() > 0) {
getFragmentManager().popBackStack();
} else {
finish();
}
}
For all the code click on the link: "http://pastebin.com/97nR68Rm"
SOLUTION!
Kato thank you for you patience and help. I have now found a solution for the problem. I'm calling ReadGroupData() and ReadChatMessages() at the end (before return) in my onCreateView methods. As Kato pointed out onCreate() is not getting called on popBackStack()
In my AddToLStViewGroup the if statement for lstViewGroup is deleted so now it always sets the listView otherwise it will throw an exception for not finding the correct view, To clarifying:
Deleted this line:
if (lstViewGroup == null) {
lstViewGroup = (ListView)getView().findViewById(R.id.listView_group);
}
And replaced with:
ListView lstViewGroup=(ListView)getView().findViewById(R.id.listView_group);
Kato thank you for you patience and help. I have now found a solution for the problem. I'm calling ReadGroupData() and ReadChatMessages() at the end (before return) in my onCreateView methods. As Kato pointed out onCreate() is not getting called on popBackStack()
In my AddToLStViewGroup the if statement for listViewGroup is deleted so now it always sets the listView otherwise it will throw an exception for not finding the correct view.
To clarify:
I deleted this line:
if (lstViewGroup == null) {
lstViewGroup = (ListView)getView().findViewById(R.id.listView_group);
}
And replaced it with:
ListView lstViewGroup =(ListView)getView().findViewById(R.id.listView_group);
(The original asker posted the answer as part of the question. I'm copying it here as a matter of housekeeping.)
I have this query located in my ParseQueryBuilder object:
public ParseQuery<Event> eventsTypes() {
ParseQuery<Event> query = Event.getQuery();
query.setCachePolicy(ParseQuery.CachePolicy.CACHE_ELSE_NETWORK);
query.setMaxCacheAge(TimeUnit.DAYS.toMillis(1));
query.whereEqualTo(Event.owner, parse.getParseUser());
query.orderByDescending(Event.timesUsed);
return query;
}
I use it to populate a ParseQueryAdapter
and at some point I would like to add an Event and immediately show it:
#OnClick(R.id.add)
public void add(Button button) {
final Event new_type = new Event();
new_type.setOwner(parse.getParseUser());
new_type.setName("atest");
new_type.saveEventually(new SaveCallback() {
#Override
public void done(ParseException e) {
if (e == null) {
// on successfull save, clear cache
parseQueryBuilder.eventsTypes().clearCachedResult();
// and show newly added object
mAdapter.loadObjects();
Toast.makeText(getActivity(), new_type.getName(), Toast.LENGTH_SHORT).show();
}
}
});
}
I expected clearing the cache would result in a new network query, revealing the newly added item but no matter what I try, it seems it will only show the initially cached result.
Even if I try to restart my app, it shows the result from the first cache.
I have a custom ListView layout that contains an ImageView and two buttons; set primary and rotate image.
What I expect it behave is when a user clicked Set Primary, the set primary button will be invisible. For the rotate button, it will refresh the imageview by rotating it 0, 90, 180, 270 degrees.
All of these processes are being done by the server. So, I have a request to a web service and the response would change the state of the UI.
Here is the listview adapter:
#Override
public View getView(int i, View view, ViewGroup viewGroup) {
...
ItemImageModel iim = items.get(i);
if (iim.getPrimary().equals("0")){
vh.btnPrimary.setVisibility(View.VISIBLE);
} else {
vh.btnPrimary.setVisibility(View.INVISIBLE);
}
vh.btnRotate.setVisibility(View.VISIBLE);
Picasso.with(context) //
.load(iim.getImage50()) //
.placeholder(R.drawable.placeholder) //
.error(R.drawable.error)
.into(vh.ivPhoto);
vh.btnPrimary.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
Log.d(this.getClass().getSimpleName(), "set primary");
// SetAsPrimary();
}
});
vh.btnRotate.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
Log.d(this.getClass().getSimpleName(), "rotate");
// RotateImage();
}
});
return view;
}
JSON response after successful primary image set:
{
"results": "Successfully set primary image",
"errors": ""
}
JSON response after successful image rotation. Degree is sent via POST parameter:
{
"results": "Successfully set rotate image",
"errors": ""
}
How do I refresh the listview with the updated UI according to the new JSON response? Do I need to refresh the ListFragment that uses this adapter? If anything please let me know.
You'll make your Http Asynchronous call inside of your onClickListener. Your http call should have a return callback that will get fired once there is a response from the server. In that callback, you can then update the UI accordingly. This however works only if you have an HttpClient that is set up nicely for callbacks. If you don't have one, I'd check out retrofit.
Here's a little example pseudocode:
vh.btnPrimary.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
Log.d(this.getClass().getSimpleName(), "set primary");
MyHttpClient.post(Url url, MyDetails details, new MyCallback() {
private void onSuccess(JSONBody body) {
updateUiPrimary();
}
private void onFailure(JSONBody body) {
failedApiCall();
}
}
}
});
vh.btnRotate.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
Log.d(this.getClass().getSimpleName(), "rotate");
MyHttpClient.post(Url url, int rotation, new MyCallback() {
private void onSuccess(JSONBody body) {
updateUiRotated();
}
private void onFailure(JSONBody body) {
failedApiCall();
}
}
}
});
I'm not sure how you make your post calls, or how you post parameters to the server, so I put in dummy arguments url, details, body, etc. It will differ based on how you implement http calls
Every time user clicks call the async task to fetch the desired image and the fetch that image to the adapter. You have to use case statement in that you might create 4 async task or pass the url to the async task.