22

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

                }
            });
        }
    };
55597
  • 2,033
  • 1
  • 21
  • 40
  • 3
    You should provide the layout of list items (xml code) and the code of your Adapter. –  Aug 04 '15 at 13:17
  • ... especially the code how your async task changes the botton and how MyAdapter.onBindViewHolder(ViewHolder holder, int position) looks like – k3b Aug 04 '15 at 13:37
  • yes buddy without xml and adapter code, we can't assist you....so post your code first – Reprator Aug 06 '15 at 03:10
  • 1
    Apologies guys. Was a little busy with other stuff. Kindly let me know if I should provide any further information . Thanks. – 55597 Aug 10 '15 at 06:57
  • 1
    In Parse SaveCallback you are updating button background and other things, but can you tell me where you update your comments list item ? – Vishal Makasana Aug 10 '15 at 10:16

3 Answers3

31

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.

Andrei Tudor Diaconu
  • 2,157
  • 21
  • 26
5

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.

Vasiliy
  • 16,221
  • 11
  • 71
  • 127
0

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.

Larry Schiefer
  • 15,687
  • 2
  • 27
  • 33