• After 15+ years, we've made a big change: Android Forums is now Early Bird Club. Learn more here.

RecyclerView issue, extra item gets selected

I have a RecyclerView to display a list of items. When I click on an item, the item will display an image, when I click on the item again, the image will be 'hidden'. This is working for the item I click on. However, when I click on an item to display the image, another item further down the list also gets it's image displayed which is not what I want. For example, if I have a list of 15 items and I click on item 1, it's image will be updated, but item 11 will also get its image updated. I put breakpoints in the onClick method but it only gets called once for the correct item so I'm not sure how the other item is getting updated. Any ideas?

ADAPTER CONFIGURATION
Code:
mDataSet = getSavedChecklists();
toDelete = new boolean[ mDataSet.size() ];
ChecklistDeleteAdapter mListAdapter = new ChecklistDeleteAdapter( getContext(), mDataSet, R.layout.item_checklist, toDelete, mDeleteBtn );
mRecyclerView.setAdapter( mListAdapter );

ADAPTER AND VIEWHOLDER
Code:
public class ChecklistDeleteAdapter extends RecyclerView.Adapter<ChecklistDeleteAdapter.ViewHolder> {

    /** Store a member variable for the list. */
    private List<Checklist> mDataSet;

    /** Context reference. */
    private Context mContext;

    /** Keeps track of what items will be deleted. */
    private boolean[] mToDelete;

    /** Screen's delete button. */
    private Button mDeleteBtn;

    /** Layout Id. */
    private int mLayoutId;

    /**************************************************************************
     * @param  context  Context
     * @param  items    List<Checklist>
     **************************************************************************/
    public ChecklistDeleteAdapter( Context context, List<Checklist> items, int layoutId, boolean[] toDelete, Button deleteBtn ) {
        mDataSet = items;
        mContext = context;
        mToDelete = toDelete;
        mDeleteBtn = deleteBtn;
        mLayoutId = layoutId;
    }

    /**************************************************************************
     * Called when RecyclerView needs a new RecyclerView.ViewHolder of the
     * given type to represent an item.
     * @param  parent    ViewGroup
     * @param  viewType  int
     * @return ChecklistDeleteAdapter.ViewHolder
     **************************************************************************/
    @NonNull
    @Override
    public ChecklistDeleteAdapter.ViewHolder onCreateViewHolder( @NonNull ViewGroup parent, int viewType ) {
        View view = LayoutInflater.from( parent.getContext() ).inflate( mLayoutId, parent, false );
        return new ViewHolder( view );
    }

    /**************************************************************************
     * Called by RecyclerView to display the data at the specified position.
     * Populates data into the item through the holder.
     * @param  viewHolder  ChecklistDeleteAdapter.ViewHolder
     * @param  position    int
     **************************************************************************/
    @Override
    public void onBindViewHolder( @NonNull final ChecklistDeleteAdapter.ViewHolder viewHolder, int position ) {
        Checklist list = mDataSet.get( position );
        viewHolder.name.setText( list.getName() );
    }

    /**************************************************************************
     * Updates delete button text with number of Checklists to delete.
     **************************************************************************/
    private void updateBtnTxt() {
        int selected = 0;
        for ( final boolean toggled : mToDelete ) {
            if ( toggled ) {
                selected += 1;
            }
        }
        if ( selected == 0 ) {
            mDeleteBtn.setText( mContext.getResources().getString( R.string.delete ) );
        } else {
            mDeleteBtn.setText( String.format( mContext.getResources().getString( R.string.sub_delete ), selected ) );
        }
    }

    /**************************************************************************
     * Returns the total number of items in the data set held by the adapter.
     * @return int the item count
     **************************************************************************/
    @Override
    public int getItemCount() {
        return mDataSet.size();
    }

    /**************************************************************************
     * Provides a direct reference to each of the views within a data item.
     * Used to cache the views within the item layout for fast access
     **************************************************************************/
    class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {

        public TextView name;
        public ImageView delImage;
        private ItemClickListener itemClickListener;

        /**********************************************************************
         * A constructor that accepts the entire item row and does the view
         * lookups to find each subview.
         * @param  itemView  View
         **********************************************************************/
        private ViewHolder( View itemView ) {
            super( itemView );
            name = itemView.findViewById( R.id.text );
            delImage = itemView.findViewById( R.id.right_image );

            itemView.setOnClickListener( this );
        }

        private void setItemClickListener( ItemClickListener itemClickListener ) {
            this.itemClickListener = itemClickListener;
        }

        @Override
        public void onClick( View view ) {
            int position = getAdapterPosition();
            if ( mToDelete[ position ] ) {

                delImage.setImageResource( R.drawable.filler );
                Log.v("sb", "Filler " + position);
            } else {
                delImage.setImageResource( R.drawable.ic_clear );
                Log.v("sb", "X-image " + position);
            }
            // toggle values
            mToDelete[ position ] = !mToDelete[ position ];
            updateBtnTxt();
        }
    }
}

ITEM LAYOUT
Code:
<?xml version="1.0" encoding="utf-8"?>
<!-- Used to display items in a list. -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/item_layout"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingTop="10dp"
    android:paddingBottom="10dp">

    <ImageView
        android:id="@+id/left_image"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="16dp"
        android:layout_marginStart="8dp"
        android:contentDescription="@string/empty_image"/>

    <TextView
        android:id="@+id/text"
        style="@style/Lists"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:layout_marginTop="7dp"
        />

    <ImageView
        android:id="@+id/right_image"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="8dp"
        android:layout_marginStart="16dp"
        android:contentDescription="@string/empty_image" />

</LinearLayout>
 
I found the issue and fixed the problem. I read a very good article https://android.jlelse.eu/anatomy-of-recyclerview-part-1-a-search-for-a-viewholder-404ba3453714 and realized my views were getting reused when items scrolled into view. I updated my onBindViewHolder() to fix the issue. I just had to check which image to load for the view at a given position.

Code:
public void onBindViewHolder( @NonNull final ChecklistDeleteAdapter.ViewHolder viewHolder, int position ) {
    Checklist list = mDataSet.get( position );

    // called when item scrolls back into view
    viewHolder.name.setText( list.getName() );
    if ( mToDelete[ position ] ) {
        viewHolder.delImage.setImageResource( R.drawable.ic_clear );
    } else {
        viewHolder.delImage.setImageResource( R.drawable.filler );
    }
}
 
Back
Top Bottom