Search code examples
javaandroidandroid-recyclerviewandroid-asynctaskindexoutofboundsexception

IndexOutOfBoundsException after refilling RecyclerView with AsyncTask and notifyItemInserted()


I have a RecyclerView that I fill with data using an AsyncTask. When I clear the List with clear() and mAdapter.notifyDataSetChanged() and then try to fill it again with the AsyncTask, I get this exception:

java.lang.IndexOutOfBoundsException: Inconsistency detected. Invalid view holder adapter position

Replacing notifyItemInserted() with notifyDataSetChanged() inside my AsyncTask solves the problem, but I don't think that's a good solution, and I'd like to understand why the first method doesn't work.

My AsyncTask doInBackground() method:

@Override
        protected Void doInBackground(Void... voids) {
            for (int i = 0; i < 100; i++) {
                mDataContainer.addItem(i);
                publishProgress(i);
            }
            return null;
        }

and my AsyncTask onProgressUpdate() method:

@Override
        protected void onProgressUpdate(Integer... values) {
            mAdapter.notifyItemInserted(values[0]);
            super.onProgressUpdate(values);
        }

I hope someone can help me with this. Thanks in advance.


Edit: Here is the adapter:

private class MyAdapter extends RecyclerView.Adapter<MyAdapter.MyViewHolder> {
        private List<Item> mItems;
        private int selectedPos = RecyclerView.NO_POSITION;

        private class MyViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
            private Item mItem;
            private TextView mNameTextView;
            private TextView mMembersTextView;

            MyViewHolder(View view) {
                super(view);
                itemView.setOnClickListener(this);
                mNameTextView = itemView.findViewById(R.id.item_name);
                mMembersTextView = itemView.findViewById(R.id.item_members);
            }

            @Override
            public void onClick(View view) {
                notifyItemChanged(selectedPos);
                selectedPos = getLayoutPosition();
                notifyItemChanged(selectedPos);
                mOnItemSelectedListener.onItemSelected(mItem);
            }
        }

        MyAdapter(List<Item> items) {
            mItems = items;
        }

        @NonNull
        @Override
        public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
            View view = LayoutInflater.from(getActivity())
                    .inflate(R.layout.list_item, parent, false);

            return new MyViewHolder(view);
        }

        @Override
        public void onBindViewHolder(MyViewHolder myViewHolder, int position) {
            Item item = mItems.get(position);
            myViewHolder.mItem = item;
            myView.mNameTextView.setText(item.getName());
            myView.mMembersTextView.setText(String.format(Locale.US,"%d/50", item.getMembers()));

            if (!mIsInit) {
                // select item that was selected before orientation change
                if (selectedPos != RecyclerView.NO_POSITION) {
                    Item selectedItem = mItems.get(selectedPos);
                    mOnItemSelectedListener.onItemSelected(selectedItem);
                // else select item 0 as default on landscape mode
                } else if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE){
                    selectedPos = 0;
                    mOnItemSelectedListener.onItemSelected(MyViewHolder.mItem);
                }
                mIsInit = true;
            }

            if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
                myViewHolder.itemView.setSelected(selectedPos == position);
            }
        }

        @Override
        public int getItemCount() {
            return mItems.size();
        }
    }

Solution

  • You should only be adding items to the adapter (and notifying) from the UI thread. Changing the adapter's backing data from the background and then notifying on the UI thread later via onProgressUpdate is quite likely to result in race conditions. In this case, mDataContainer.addItem(i); should be moved to onProgressUpdate before you notify the adapter.

    It's hard to confirm if that's the only issue without seeing the definition of mDataContainer, but fixing the synchronization here would definitely be the first step toward fixing this.