Home » Android » android – How is the position of a RecyclerView adapter related to the index of its dataset?

android – How is the position of a RecyclerView adapter related to the index of its dataset?

Posted by: admin May 14, 2020 Leave a comment

Questions:

I thought they were the same, but they’re not. The following code gives an indexOutOfBounds exception when I try to access the “position” index of my dataset, in this case a list of a model I created called Task:

public class TaskAdapter extends RecyclerView.Adapter<TaskAdapter.TaskViewHolder>   {

private List<Task> taskList;
private TaskAdapter thisAdapter = this;

// cache of views to reduce number of findViewById calls
public static class TaskViewHolder extends RecyclerView.ViewHolder {
    protected TextView taskTV;
    protected ImageView closeBtn;

    public TaskViewHolder(View v) {
        super(v);
        taskTV = (TextView)v.findViewById(R.id.taskDesc);
        closeBtn = (ImageView)v.findViewById(R.id.xImg);
    }
}


public TaskAdapter(List<Task> tasks) {
    if(tasks == null)
        throw new IllegalArgumentException("tasks cannot be null");
    taskList = tasks;
}


// onBindViewHolder binds a model to a viewholder
@Override
public void onBindViewHolder(TaskViewHolder taskViewHolder, int pos) {
    final int position = pos;
    Task currTask = taskList.get(pos);
    taskViewHolder.taskTV.setText(currTask.getDescription());

    **taskViewHolder.closeBtn.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Log.d("TRACE", "Closing task at position " + position);
            // delete from SQLite DB
            Task taskToDel = taskList.get(position);
            taskToDel.delete();
            // updating UI
            taskList.remove(position);
            thisAdapter.notifyItemRemoved(position);
        }
    });**
}

@Override
public int getItemCount() {
    //Log.d("TRACE", taskList.size() + " tasks in DB");
    return taskList.size();
}


// inflates row to create a viewHolder
@Override
public TaskViewHolder onCreateViewHolder(ViewGroup parent, int pos) {
    View itemView = LayoutInflater.from(parent.getContext()).
                                   inflate(R.layout.list_item, parent, false);
    Task currTask = taskList.get(pos);

    //itemView.setBackgroundColor(Color.parseColor(currTask.getColor()));
    return new TaskViewHolder(itemView);
}
}

Deleting from my recyclerview gives unexpected results sometimes. Sometimes the element ahead of the one clicked is deleted, other times an indexOutOfBounds exception occurs at “taskList.get(position)”.

Reading https://developer.android.com/reference/android/support/v7/widget/RecyclerView.Adapter.html and https://developer.android.com/training/material/lists-cards.html did not give me any more insight into why this was happening and how to fix it.

It looks like RecyclerView recycles the rows, but I wouldn’t expect an indexoutofbounds exception using a smaller subset of numbers to index my list.

How to&Answers:

RecyclerView does not rebind views when their positions change (for obvious performance reasons).
For example, if your data set looks like this:

A B C D

and you add item X via

mItems.add(1, X);
notifyItemInserted(1, 1);

to get

A X B C D

RecyclerView will only bind X and run the animation.

There is a getPosition method in ViewHolder but that may not match adapter position if you call it in the middle of an animation.

If you need the adapter position, your safest option is getting the position from the Adapter.

update for your comment

Add a Task field to the ViewHolder.

Change onCreateViewHolder as follows to avoid creating a listener object on each rebind.

// inflates row to create a viewHolder
@Override
public TaskViewHolder onCreateViewHolder(ViewGroup parent, int type) {
    View itemView = LayoutInflater.from(parent.getContext()).
                               inflate(R.layout.list_item, parent, false);

    final TaskViewHolder vh = new TaskViewHolder(itemView);
    taskViewHolder.closeBtn.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            // delete from SQLite DB
            Task taskToDel = vh.getTask();
            final int pos = taskList.indexOf(taskToDel);
            if (pos == -1) return;
            taskToDel.delete();
            // updating UI
            taskList.remove(pos);
            thisAdapter.notifyItemRemoved(pos);
        }
    });
}

so in your on bind method, you do

// onBindViewHolder binds a model to a viewholder
@Override
public void onBindViewHolder(TaskViewHolder taskViewHolder, int pos) {
    Task currTask = taskList.get(pos);
    taskViewHolder.setTask(currTask);
    taskViewHolder.taskTV.setText(currTask.getDescription());
}

Answer:

Like yigit said, RecyclerView works like that:

A B C D

and you add item X via

mItems.add(1, X);
notifyItemInserted(1, 1);

you get

A X B C D

Using holder.getAdapterPosition() in onClickListener() will give you the right item from dataset to be removed, not the “static” view position. Here’s the doc about it onBindViewHolder

Answer:

Why dont you use a public interface for the button click and controle the action in the MainActivity.

In your adapter add:

public interface OnItemClickListener {
    void onItemClick(View view, int position, List<Task> mTaskList);
}

and

public OnItemClickListener mItemClickListener;

// Provide a suitable constructor (depends on the kind of dataset)
public TaskAdapter (List<Task> myDataset, OnItemClickListener mItemClickListener) {
    this.mItemClickListener = mItemClickListener;
    this.mDataset = mDataset;
}

plus the call in the ViewHolder class

 public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {

    public ViewHolder(View v) {
        super(v);
        ...
        closeBtn = (ImageView)v.findViewById(R.id.xImg);
        closeBtn.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        // If not long clicked, pass last variable as false.
        mItemClickListener.onItemClick(v, getAdapterPosition(), mDataset);
    }
}

In your MainActivity change your adapter to handle the call

// set Adapter
    mAdapter = new TaskAdapter(taskList, new TaskAdapter.OnItemClickListener() {

        @Override
        public void onItemClick(View v, int position) {
            if (v.getId() == R.id.xImg) {
                Task taskToDel = taskList.get(position);
                // updating UI
                taskList.remove(position);
                thisAdapter.notifyItemRemoved(position);
                // remove from db with unique id to use delete query
                // dont use the position but something like taskToDel.getId() 
                taskToDel.delete();
            } 
        }
    });

Answer:

Personally, I don’t like this concept of RecyclerViews. Seems like it wasn’t thought of completely.

As it was said when removing an item the Recycler view just hides an item. But usually you don’t want to leave that item in your collection. When deleting an item from the collection “it shifts its elements towards 0” whereas recyclerView keeps the same size.

If you are calling taskList.remove(position); your position must be evaluated again:

int position = recyclerView.getChildAdapterPosition(taskViewHolder.itemView);

Answer:

Thanks to @yigit for his answer, his solution mainly worked, I just tweaked it a little bit so as to avoid using vh.getTask() which I was not sure how to implement.

    final ViewHolder vh = new ViewHolder(customView);
    final KittyAdapter final_copy_of_this = this;

    // We attach a CheckChange Listener here instead of onBindViewHolder
    // to avoid creating a listener object on each rebind
    // Note Rebind is only called if animation must be called on view (for efficiency)
    // It does not call on the removed if the last item is checked
    vh.done.setChecked(false);
    vh.done.setOnCheckedChangeListener(null);
    vh.done.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
        @Override
        public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
            buttonView.setEnabled(false);
            final int pos2 = vh.getAdapterPosition(); // THIS IS HOW TO GET THE UPDATED POSITION

            // YOU MUST UPDATE THE DATABASE, removed by Title
            DatabaseHandler db = new DatabaseHandler(mContext);
            db.remove(mDataSet.get(pos2).getTitle(), fp);
            db.close();
            // Update UI
            mDataSet.remove(pos2);
            final_copy_of_this.notifyItemRemoved(pos2);

        }
    });

Notice instead to get the updated position, you can call vh.getAdapterPosition(), which is the line that will give you the updated position from the underlying dataset rather than the fake view.

This is working for me as of now, if someone knows of a drawback to using this please let me know. Hope this helps someone.