348

I have a recycler view that works perfectly on all devices except Samsung. On Samsung, I'm get

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

when I'm going back to the fragment with the recycler view from another activity.

Adapter code:

public class FeedRecyclerAdapter extends RecyclerView.Adapter<FeedRecyclerAdapter.MovieViewHolder> {
    public static final String getUserPhoto = APIConstants.BASE_URL + APIConstants.PICTURE_PATH_SMALL;
    Movie[] mMovies = null;
    Context mContext = null;
    Activity mActivity = null;
    LinearLayoutManager mManager = null;
    private Bus uiBus = null;
    int mCountOfLikes = 0;

    //Constructor
    public FeedRecyclerAdapter(Movie[] movies, Context context, Activity activity,
                               LinearLayoutManager manager) {
        mContext = context;
        mActivity = activity;
        mMovies = movies;
        mManager = manager;
        uiBus = BusProvider.getUIBusInstance();
    }

    public void setMoviesAndNotify(Movie[] movies, boolean movieIgnored) {
        mMovies = movies;
        int firstItem = mManager.findFirstVisibleItemPosition();
        View firstItemView = mManager.findViewByPosition(firstItem);
        int topOffset = firstItemView.getTop();
        notifyDataSetChanged();
        if(movieIgnored) {
            mManager.scrollToPositionWithOffset(firstItem - 1, topOffset);
        } else {
            mManager.scrollToPositionWithOffset(firstItem, topOffset);
        }
    }

    // Create new views (called by layout manager)
    @Override
    public MovieViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.feed_one_recommended_movie_layout, parent, false);

        return new MovieViewHolder(view);
    }

    // Replaced contend of each view (called by layout manager)
    @Override
    public void onBindViewHolder(MovieViewHolder holder, int position) {
        setLikes(holder, position);
        setAddToCollection(holder, position);
        setTitle(holder, position);
        setIgnoreMovieInfo(holder, position);
        setMovieInfo(holder, position);
        setPosterAndTrailer(holder, position);
        setDescription(holder, position);
        setTags(holder, position);
    }

    // returns item count (called by layout manager)
    @Override
    public int getItemCount() {
        return mMovies != null ? mMovies.length : 0;
    }

    private void setLikes(final MovieViewHolder holder, final int position) {
        List<Reason> likes = new ArrayList<>();
        for(Reason reason : mMovies[position].reasons) {
            if(reason.title.equals("Liked this movie")) {
                likes.add(reason);
            }
        }
        mCountOfLikes = likes.size();
        holder.likeButton.setText(mContext.getString(R.string.like)
            + Html.fromHtml(getCountOfLikesString(mCountOfLikes)));
        final MovieRepo repo = MovieRepo.getInstance();
        final int pos = position;
        final MovieViewHolder viewHolder = holder;
        holder.likeButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if(mMovies[pos].isLiked) {
                    repo.unlikeMovie(AuthStore.getInstance()
                        .getAuthToken(), mMovies[pos].id, new Callback<Movie>() {
                        @Override
                        public void success(Movie movie, Response response) {
                            Drawable img = mContext.getResources().getDrawable(R.drawable.ic_like);
                            viewHolder.likeButton
                                .setCompoundDrawablesWithIntrinsicBounds(img, null, null, null);
                            if (--mCountOfLikes <= 0) {
                                viewHolder.likeButton.setText(mContext.getString(R.string.like));
                            } else {
                                viewHolder.likeButton
                                    .setText(Html.fromHtml(mContext.getString(R.string.like)
                                        + getCountOfLikesString(mCountOfLikes)));
                            }
                            mMovies[pos].isLiked = false;
                        }

                        @Override
                        public void failure(RetrofitError error) {
                            Toast.makeText(mContext.getApplicationContext(),
                                mContext.getString(R.string.cannot_like), Toast.LENGTH_LONG)
                                .show();
                        }
                    });
                } else {
                    repo.likeMovie(AuthStore.getInstance()
                        .getAuthToken(), mMovies[pos].id, new Callback<Movie>() {
                        @Override
                        public void success(Movie movie, Response response) {
                            Drawable img = mContext.getResources().getDrawable(R.drawable.ic_liked_green);
                            viewHolder.likeButton
                                .setCompoundDrawablesWithIntrinsicBounds(img, null, null, null);
                            viewHolder.likeButton
                                .setText(Html.fromHtml(mContext.getString(R.string.like)
                                    + getCountOfLikesString(++mCountOfLikes)));
                            mMovies[pos].isLiked = true;
                            setComments(holder, position);
                        }

                        @Override
                        public void failure(RetrofitError error) {
                            Toast.makeText(mContext,
                                mContext.getString(R.string.cannot_like), Toast.LENGTH_LONG).show();
                        }
                    });
                }
            }
        });
    }

    private void setComments(final MovieViewHolder holder, final int position) {
        holder.likeAndSaveButtonLayout.setVisibility(View.GONE);
        holder.commentsLayout.setVisibility(View.VISIBLE);
        holder.sendCommentButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (holder.commentsInputEdit.getText().length() > 0) {
                    CommentRepo repo = CommentRepo.getInstance();
                  repo.sendUserComment(AuthStore.getInstance().getAuthToken(), mMovies[position].id,
                        holder.commentsInputEdit.getText().toString(), new Callback<Void>() {
                            @Override
                            public void success(Void aVoid, Response response) {
                                Toast.makeText(mContext, mContext.getString(R.string.thanks_for_your_comment),
                                    Toast.LENGTH_SHORT).show();
                                hideCommentsLayout(holder);
                            }

                            @Override
                            public void failure(RetrofitError error) {
                                Toast.makeText(mContext, mContext.getString(R.string.cannot_add_comment),
                                    Toast.LENGTH_LONG).show();
                            }
                        });
                } else {
                    hideCommentsLayout(holder);
                }
            }
        });
    }

    private void hideCommentsLayout(MovieViewHolder holder) {
        holder.commentsLayout.setVisibility(View.GONE);
        holder.likeAndSaveButtonLayout.setVisibility(View.VISIBLE);
    }

    private void setAddToCollection(final MovieViewHolder holder, int position) {
        final int pos = position;
        if(mMovies[position].isInWatchlist) {
            holder.saveButton
              .setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_check_green, 0, 0, 0);
        }
        final CollectionRepo repo = CollectionRepo.getInstance();
        holder.saveButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if(!mMovies[pos].isInWatchlist) {
                   repo.addMovieToCollection(AuthStore.getInstance().getAuthToken(), 0, mMovies[pos].id, new Callback<MovieCollection[]>() {
                            @Override
                            public void success(MovieCollection[] movieCollections, Response response) {
                                holder.saveButton
                                    .setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_check_green, 0, 0, 0);

                                mMovies[pos].isInWatchlist = true;
                            }

                            @Override
                            public void failure(RetrofitError error) {
                                Toast.makeText(mContext, mContext.getString(R.string.movie_not_added_to_collection),
                                    Toast.LENGTH_LONG).show();
                            }
                        });
                } else {
                 repo.removeMovieFromCollection(AuthStore.getInstance().getAuthToken(), 0,
                        mMovies[pos].id, new Callback<MovieCollection[]>() {
                        @Override
                        public void success(MovieCollection[] movieCollections, Response response) {
                            holder.saveButton
                                .setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_plus, 0, 0, 0);

                            mMovies[pos].isInWatchlist = false;
                        }

                        @Override
                        public void failure(RetrofitError error) {
                            Toast.makeText(mContext,
                                mContext.getString(R.string.cannot_delete_movie_from_watchlist),
                                Toast.LENGTH_LONG).show();
                        }
                    });
                }
            }
        });
    }

    private String getCountOfLikesString(int countOfLikes) {
        String countOfLikesStr;
        if(countOfLikes == 0) {
            countOfLikesStr = "";
        } else if(countOfLikes > 999) {
            countOfLikesStr = " " + (countOfLikes/1000) + "K";
        } else if (countOfLikes > 999999){
            countOfLikesStr = " " + (countOfLikes/1000000) + "M";
        } else {
            countOfLikesStr = " " + String.valueOf(countOfLikes);
        }
        return "<small>" + countOfLikesStr + "</small>";
    }

    private void setTitle(MovieViewHolder holder, final int position) {
        holder.movieTitleTextView.setText(mMovies[position].title);
        holder.movieTitleTextView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                MovieDetailActivity.openView(mContext, mMovies[position].id, true, false);
            }
        });
    }

    private void setIgnoreMovieInfo(MovieViewHolder holder, final int position) {
        holder.ignoreMovie.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                MovieRepo repo = MovieRepo.getInstance();
                repo.hideMovie(AuthStore.getInstance().getAuthToken(), mMovies[position].id,
                    new Callback<Void>() {
                        @Override
                        public void success(Void aVoid, Response response) {
                            Movie[] newMovies = new Movie[mMovies.length - 1];
                            for (int i = 0, j = 0; j < mMovies.length; i++, j++) {
                                if (i != position) {
                                    newMovies[i] = mMovies[j];
                                } else {
                                    if (++j < mMovies.length) {
                                        newMovies[i] = mMovies[j];
                                    }
                                }
                            }
                            uiBus.post(new MoviesChangedEvent(newMovies));
                            setMoviesAndNotify(newMovies, true);
                            Toast.makeText(mContext, mContext.getString(R.string.movie_ignored),
                                Toast.LENGTH_SHORT).show();
                        }

                        @Override
                        public void failure(RetrofitError error) {
                            Toast.makeText(mContext, mContext.getString(R.string.movie_ignored_failed),
                                Toast.LENGTH_LONG).show();
                        }
                    });
            }
        });
    }

    private void setMovieInfo(MovieViewHolder holder, int position) {
        String imdp = "IMDB: ";
        String sources = "", date;
        if(mMovies[position].showtimes != null && mMovies[position].showtimes.length > 0) {
            int countOfSources = mMovies[position].showtimes.length;
            for(int i = 0; i < countOfSources; i++) {
                sources += mMovies[position].showtimes[i].name + ", ";
            }
            sources = sources.trim();
            if(sources.charAt(sources.length() - 1) == ',') {
                if(sources.length() > 1) {
                    sources = sources.substring(0, sources.length() - 2);
                } else {
                    sources = "";
                }
            }
        } else {
            sources = "";
        }
        imdp += mMovies[position].imdbRating + " | ";
        if(sources.isEmpty()) {
            date = mMovies[position].releaseYear;
        } else {
            date = mMovies[position].releaseYear + " | ";
        }

        holder.movieInfoTextView.setText(imdp + date + sources);
    }

    private void setPosterAndTrailer(final MovieViewHolder holder, final int position) {
        if (mMovies[position] != null && mMovies[position].posterPath != null
            && !mMovies[position].posterPath.isEmpty()) {
            Picasso.with(mContext)
                .load(mMovies[position].posterPath)
             .error(mContext.getResources().getDrawable(R.drawable.noposter))
                .into(holder.posterImageView);
        } else {
            holder.posterImageView.setImageResource(R.drawable.noposter);
        }
        holder.posterImageView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                MovieDetailActivity.openView(mActivity, mMovies[position].id, false, false);
            }
        });
        if(mMovies[position] != null && mMovies[position].trailerLink  != null
            && !mMovies[position].trailerLink.isEmpty()) {
            holder.playTrailer.setVisibility(View.VISIBLE);
            holder.playTrailer.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    MovieDetailActivity.openView(mActivity, mMovies[position].id, false, true);
                }
            });
        }
    }

    private void setDescription(MovieViewHolder holder, int position) {
        String text = mMovies[position].overview;
        if(text == null || text.isEmpty()) {
       holder.descriptionText.setText(mContext.getString(R.string.no_description));
        } else if(text.length() > 200) {
            text = text.substring(0, 196) + "...";
            holder.descriptionText.setText(text);
        } else {
            holder.descriptionText.setText(text);
        }
        final int pos = position;
        holder.descriptionText.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                MovieDetailActivity.openView(mActivity, mMovies[pos].id, false, false);
            }
        });
    }

    private void setTags(MovieViewHolder holder, int position) {
        List<String> tags = Arrays.asList(mMovies[position].tags);
        if(tags.size() > 0) {
            CastAndTagsFeedAdapter adapter = new CastAndTagsFeedAdapter(tags,
                mContext, ((FragmentActivity) mActivity).getSupportFragmentManager());
            holder.tags.setItemMargin(10);
            holder.tags.setAdapter(adapter);
        } else {
            holder.tags.setVisibility(View.GONE);
        }
    }

    // class view holder that provide us a link for each element of list
    public static class MovieViewHolder extends RecyclerView.ViewHolder {
        TextView movieTitleTextView, movieInfoTextView, descriptionText, reasonsCountText;
        TextView reasonText1, reasonAuthor1, reasonText2, reasonAuthor2;
        EditText commentsInputEdit;
        Button likeButton, saveButton, playTrailer, sendCommentButton;
        ImageButton ignoreMovie;
        ImageView posterImageView, userPicture1, userPicture2;
        TwoWayView tags;
        RelativeLayout mainReasonsLayout, firstReasonLayout, secondReasonLayout, reasonsListLayout;
        RelativeLayout commentsLayout;
        LinearLayout likeAndSaveButtonLayout;
        ProgressBar progressBar;

        public MovieViewHolder(View view) {
            super(view);
            movieTitleTextView = (TextView)view.findViewById(R.id.movie_title_text);
            movieInfoTextView = (TextView)view.findViewById(R.id.movie_info_text);
            descriptionText = (TextView)view.findViewById(R.id.text_description);
            reasonsCountText = (TextView)view.findViewById(R.id.reason_count);
            reasonText1 = (TextView)view.findViewById(R.id.reason_text_1);
            reasonAuthor1 = (TextView)view.findViewById(R.id.author_1);
            reasonText2 = (TextView)view.findViewById(R.id.reason_text_2);
            reasonAuthor2 = (TextView)view.findViewById(R.id.author_2);
            commentsInputEdit = (EditText)view.findViewById(R.id.comment_input);
            likeButton = (Button)view.findViewById(R.id.like_button);
            saveButton = (Button)view.findViewById(R.id.save_button);
            playTrailer = (Button)view.findViewById(R.id.play_trailer_button);
            sendCommentButton = (Button)view.findViewById(R.id.send_button);
            ignoreMovie = (ImageButton)view.findViewById(R.id.ignore_movie_imagebutton);
            posterImageView = (ImageView)view.findViewById(R.id.poster_image);
            userPicture1 = (ImageView)view.findViewById(R.id.user_picture_1);
            userPicture2 = (ImageView)view.findViewById(R.id.user_picture_2);
            tags = (TwoWayView)view.findViewById(R.id.list_view_feed_tags);
            mainReasonsLayout = (RelativeLayout)view.findViewById(R.id.reasons_main_layout);
            firstReasonLayout = (RelativeLayout)view.findViewById(R.id.first_reason);
            secondReasonLayout = (RelativeLayout)view.findViewById(R.id.second_reason);
            reasonsListLayout = (RelativeLayout)view.findViewById(R.id.reasons_list);
            commentsLayout = (RelativeLayout)view.findViewById(R.id.comments_layout);
            likeAndSaveButtonLayout = (LinearLayout)view
                .findViewById(R.id.like_and_save_buttons_layout);
            progressBar = (ProgressBar)view.findViewById(R.id.centered_progress_bar);
        }
    }
}

Exception:

java.lang.IndexOutOfBoundsException: Inconsistency detected. Invalid view holder adapter positionViewHolder{42319ed8 position=1 id=-1, oldPos=0, pLpos:0 scrap tmpDetached no parent}
 at android.support.v7.widget.RecyclerView$Recycler.validateViewHolderForOffsetPosition(RecyclerView.java:4166)
 at android.support.v7.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:4297)
 at android.support.v7.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:4278)
 at android.support.v7.widget.LinearLayoutManager$LayoutState.next(LinearLayoutManager.java:1947)
 at android.support.v7.widget.GridLayoutManager.layoutChunk(GridLayoutManager.java:434)
 at android.support.v7.widget.LinearLayoutManager.fill(LinearLayoutManager.java:1322)
 at android.support.v7.widget.LinearLayoutManager.onLayoutChildren(LinearLayoutManager.java:556)
 at android.support.v7.widget.GridLayoutManager.onLayoutChildren(GridLayoutManager.java:171)
 at android.support.v7.widget.RecyclerView.dispatchLayout(RecyclerView.java:2627)
 at android.support.v7.widget.RecyclerView.onLayout(RecyclerView.java:2971)
 at android.view.View.layout(View.java:15746)
 at android.view.ViewGroup.layout(ViewGroup.java:4867)
 at android.support.v4.widget.SwipeRefreshLayout.onLayout(SwipeRefreshLayout.java:562)
 at android.view.View.layout(View.java:15746)
 at android.view.ViewGroup.layout(ViewGroup.java:4867)
 at android.widget.FrameLayout.layoutChildren(FrameLayout.java:453)
 at android.widget.FrameLayout.onLayout(FrameLayout.java:388)
 at android.view.View.layout(View.java:15746)
 at android.view.ViewGroup.layout(ViewGroup.java:4867)
 at android.support.v4.view.ViewPager.onLayout(ViewPager.java:1626)
 at android.view.View.layout(View.java:15746)
 at android.view.ViewGroup.layout(ViewGroup.java:4867)
 at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1677)
 at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1531)
 at android.widget.LinearLayout.onLayout(LinearLayout.java:1440)
 at android.view.View.layout(View.java:15746)
 at android.view.ViewGroup.layout(ViewGroup.java:4867)
 at android.support.v4.view.ViewPager.onLayout(ViewPager.java:1626)
 at android.view.View.layout(View.java:15746)
 at android.view.ViewGroup.layout(ViewGroup.java:4867)
 at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1677)
 at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1531)
 at android.widget.LinearLayout.onLayout(LinearLayout.java:1440)
 at android.view.View.layout(View.java:15746)
 at android.view.ViewGroup.layout(ViewGroup.java:4867)
 at android.widget.FrameLayout.layoutChildren(FrameLayout.java:453)
 at android.widget.FrameLayout.onLayout(FrameLayout.java:388)
 at android.view.View.layout(View.java:15746)
 at android.view.ViewGroup.layout(ViewGroup.java:4867)
 at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1677)
 at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1531)
 at android.widget.LinearLayout.onLayout(LinearLayout.java:1440)
 at android.view.View.layout(View.java:15746)
 at android.view.ViewGroup.layout(ViewGroup.java:4867)
 at android.widget.FrameLayout.layoutChildren(FrameLayout.java:453)
 at android.widget.FrameLayout.onLayout(FrameLayout.java:388)
07-30 12:48:22.688    9590-9590/com.Filmgrail.android.debug W/System.err? at android.view.View.layout(View.java:15746)
 at android.view.ViewGroup.layout(ViewGroup.java:4867)
 at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1677)
 at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1531)
 at android.widget.LinearLayout.onLayout(LinearLayout.java:1440)
 at android.view.View.layout(View.java:15746)
 at android.view.ViewGroup.layout(ViewGroup.java:4867)
 at android.widget.FrameLayout.layoutChildren(FrameLayout.java:453)
 at android.widget.FrameLayout.onLayout(FrameLayout.java:388)
 at android.view.View.layout(View.java:15746)
 at android.view.ViewGroup.layout(ViewGroup.java:4867)
 at android.view.ViewRootImpl.performLayout(ViewRootImpl.java:2356)
 at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:2069)
 at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1254)
 at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:6630)
 at android.view.Choreographer$CallbackRecord.run(Choreographer.java:803)
 at android.view.Choreographer.doCallbacks(Choreographer.java:603)
 at android.view.Choreographer.doFrame(Choreographer.java:573)
 at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:789)
 at android.os.Handler.handleCallback(Handler.java:733)
 at android.os.Handler.dispatchMessage(Handler.java:95)
 at android.os.Looper.loop(Looper.java:136)
 at android.app.ActivityThread.main(ActivityThread.java:5479)
 at java.lang.reflect.Method.invokeNative(Native Method)
 at java.lang.reflect.Method.invoke(Method.java:515)
 at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1283)
 at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1099)
 at dalvik.system.NativeStart.main(Native Method)

How can I fix this?

stkent
  • 19,772
  • 14
  • 85
  • 111
Vladimir Fisher
  • 3,090
  • 2
  • 17
  • 23

35 Answers35

240

This problem is caused by RecyclerView Data modified in different thread. The best way is checking all data access. And a workaround is wrapping LinearLayoutManager.

Previous answer

There was actually a bug in RecyclerView and the support 23.1.1 still not fixed.

For a workaround, notice that backtrace stacks, if we can catch this Exception in one of some class it may skip this crash. For me, I create a LinearLayoutManagerWrapper and override the onLayoutChildren:

public class WrapContentLinearLayoutManager extends LinearLayoutManager {
    //... constructor
    @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        try {
            super.onLayoutChildren(recycler, state);
        } catch (IndexOutOfBoundsException e) {
            Log.e("TAG", "meet a IOOBE in RecyclerView");
        }
    }
}

Then set it to RecyclerView:

RecyclerView recyclerView = (RecyclerView)findViewById(R.id.recycler_view);

recyclerView.setLayoutManager(new WrapContentLinearLayoutManager(activity, LinearLayoutManager.HORIZONTAL, false));

Actually catch this exception, and seems no any side-effect yet.

Also, if you use GridLayoutManager or StaggeredGridLayoutManager you must create a wrapper for it.

Notice: The RecyclerView may be in a wrong internal state.

sakiM
  • 4,832
  • 5
  • 27
  • 33
  • 1
    where exactly do you put this? on adapter or activity? – Steve Kamau Nov 28 '15 at 20:41
  • extend a `LinearLayoutManager` and override this. I will an addition in my answer. – sakiM Nov 30 '15 at 07:39
  • 24
    https://code.google.com/p/android/issues/detail?id=158046 answer #12 said don't do that. – Robert Dec 15 '15 at 09:09
  • ummm, you are right. It seems hard to defuse all potential non-UI-thread modifications in my app, I will keep this only as a workaround way. – sakiM Jan 11 '16 at 06:15
  • @Vlado what do you mean `not work`? Did you use the own patched LinearLayoutManager? not the system one. And this should be used in all of your `LinearLayoutManager`. – sakiM Jul 12 '16 at 16:29
  • In my case I was notifying item removal in an onAnimationEnd (which runs in a different thread?). Moved the code outside of that and the exception doesn't seem to occur yet. – Alex Newman Apr 16 '17 at 12:19
  • 1
    refer this answer #9 (https://issuetracker.google.com/issues/37030377#comment9). It worked for me. – vikoo Jun 05 '17 at 07:10
  • 1
    For my case I am doing on the same thread. mDataHolder.get().removeAll(mHiddenGenre); mAdapter.notifyItemRangeRemoved(mExpandButtonPosition, mHiddenGenre.size()); – Pinser Sep 14 '17 at 05:18
  • @sakiM How to check all data access? – Elaman Aitymbet Oct 25 '17 at 04:16
  • Not working for all devices. I don't know why this is not fixed I am very angry with latest version too .. -_- – Vaibhav Kadam Jan 12 '18 at 04:38
  • Can't believe this still works. Thanks a ton! I am only getting this issue on some Samsung devices running on API 23 – harsh_v Feb 13 '18 at 05:31
  • I guess you guys just change size of items in your list in background thread – user924 Mar 01 '18 at 23:32
  • What is the consequence of Recyclerview being in a wrong internal state. – Sreekanth Karumanaghat May 04 '18 at 06:10
  • Did anyone find the permanent solution of this? Has RecyclerView fixed this issue? – Mohit Rajput May 23 '18 at 12:35
  • Is the bug fixed now? – Foobar Jul 12 '18 at 01:59
  • This is the only solution that worked for me. I even updated recyclerView to the latest version, bug was still there. More specifically I copied https://github.com/yq008/basepro/blob/master/appLib/src/main/java/com/yq008/basepro/applib/widget/recyclerview/WrapContentLinearLayoutManager.java into a new java class and called it, as in the answer. – CHarris Aug 09 '18 at 12:27
  • I don't update data from different threads (only main) and I still get reports from Google Console about this issue. And device of course is Samsung. Galaxy J3(2017) (j3y17lte), Android 8.0 – user25 Jan 05 '19 at 23:23
105

This is an example for refreshing data with completely new content. You can easily modify it to fit your needs. I solved this in my case by calling:

notifyItemRangeRemoved(0, previousContentSize);

before:

notifyItemRangeInserted(0, newContentSize);

This is the correct solution and is also mentioned in this post by an AOSP project member.

Willi Mentzel
  • 27,862
  • 20
  • 113
  • 121
box
  • 4,450
  • 2
  • 38
  • 38
52

I faced this issue once, and I solved this by wrapping the LayoutManager and disabling predictive animations.

Here an example:

public class LinearLayoutManagerWrapper extends LinearLayoutManager {

  public LinearLayoutManagerWrapper(Context context) {
    super(context);
  }

  public LinearLayoutManagerWrapper(Context context, int orientation, boolean reverseLayout) {
    super(context, orientation, reverseLayout);
  }

  public LinearLayoutManagerWrapper(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    super(context, attrs, defStyleAttr, defStyleRes);
  }

  @Override
  public boolean supportsPredictiveItemAnimations() {
    return false;
  }
}

And set it to RecyclerView:

RecyclerView.LayoutManager mLayoutManager = new LinearLayoutManagerWrapper(context, LinearLayoutManager.VERTICAL, false);
Willi Mentzel
  • 27,862
  • 20
  • 113
  • 121
hcknl
  • 1,219
  • 10
  • 15
  • this seems to work for me, but can you tell why this works? – Dennis Anderson May 17 '18 at 11:57
  • Fixed for me too. How you predicted that this may be the cause of this crash. – Rahul Rastogi Aug 10 '19 at 11:33
  • 1
    The base class of LinearLayoutManager method supportsPredictiveAnimations() returns false by default. What are we getting by overriding the method here? `public boolean supportsPredictiveItemAnimations() { return false; }` – telaCode Dec 03 '19 at 16:45
  • 1
    @M.Hig The documentation for `LinearLayoutManager` says the default is false, but that statement is false :-( The decompiled code for `LinearLayoutManager` has this: public boolean supportsPredictiveItemAnimations() { return this.mPendingSavedState == null && this.mLastStackFromEnd == this.mStackFromEnd; } – Clyde Dec 18 '19 at 03:26
  • I use diff utils for updating my recycler view adapter and this answer fixed a crash. Thanks a lot, dear author! – Eugene P. Mar 06 '20 at 15:25
  • Thanks a ton, finally this solution worked for me in Samsung M12. – Shailendra Madda Jun 11 '21 at 17:29
49

New answer: Use DiffUtil for all RecyclerView updates. This will help with both performance and the bug above. See Here

Previous answer: This worked for me. The key is to not use notifyDataSetChanged() and to do the right things in the correct order:

public void setItems(ArrayList<Article> newArticles) {
    //get the current items
    int currentSize = articles.size();
    //remove the current items
    articles.clear();
    //add all the new items
    articles.addAll(newArticles);
    //tell the recycler view that all the old items are gone
    notifyItemRangeRemoved(0, currentSize);
    //tell the recycler view how many new items we added
    notifyItemRangeInserted(0, newArticles.size());
}
Bolling
  • 3,954
  • 1
  • 27
  • 29
  • 2
    This is the most thorough solution with a good explanation. Thanks! – Sakiboy May 07 '17 at 20:34
  • then what is the purpose of using the notifyitemrangeinserted instead of notifydatasetchanged() , @Bolling. – Ankur_009 Apr 17 '18 at 05:32
  • @FilipLuch Can you explain why? – Sreekanth Karumanaghat May 04 '18 at 05:28
  • 4
    @SreekanthKarumanaghat sure, dunno why I did not explain the reason. Basically he clears then recreates all items in the list. Like in search results, very often you get same items, or when refresh is done you get same items and then you'd end up recreating everything, which is a waste of performance. Use DiffUtils instead and only update the changes rather than all items. It's like going from A to Z every time, but you only changed the F in there. – Filip Luchianenco May 16 '18 at 15:51
  • It led me to a solution for similar problem. In my case I forgot to call adapter.notifyItemRemoved(position), after item was removed from connected list. – dorsz Sep 20 '18 at 21:22
  • 2
    DiffUtil is a hidden treasure. Thanks for sharing! – Sileria Mar 15 '19 at 15:50
  • This is not at all good for performance and UI experience. Think what happen when we need to do load more feature? – Lovekush Vishwakarma Jun 21 '19 at 12:00
  • @Ankur_009, if you use `notifyDataSetChanged()`, you avoid any animation in RecyclerView. Use DiffUtil. For animation with `notifyDataSetChanged()` you should use `setHasStableIds(true);` and override `public long getItemId(int position)`. – CoolMind Jul 02 '19 at 15:41
  • It still crashes for me with DiffUtil – mathematics-and-caffeine Feb 01 '22 at 16:32
42

Reasons caused this issue:

  1. An internal issue in Recycler when item animations are enabled
  2. Modification on Recycler data in another thread
  3. Calling notify methods in a wrong way

SOLUTION:

-----------------SOLUTION 1---------------

  • Catching the exception (Not recommended especially for reason #3)

Create a custom LinearLayoutManager as the following and set it to the ReyclerView

    public class CustomLinearLayoutManager extends LinearLayoutManager {

            //Generate constructors

            @Override
            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {

                try {

                    super.onLayoutChildren(recycler, state);

                } catch (IndexOutOfBoundsException e) {

                    Log.e(TAG, "Inconsistency detected");
                }

            }
        }

Then set RecyclerVIew Layout Manager as follow:

recyclerView.setLayoutManager(new CustomLinearLayoutManager(activity));

-----------------SOLUTION 2---------------

  • Disable item animations (fixes the issue if it caused de to reason #1):

Again, create a custom Linear Layout Manager as follow:

    public class CustomLinearLayoutManager extends LinearLayoutManager {

            //Generate constructors

             @Override
             public boolean supportsPredictiveItemAnimations() {
                 return false;
             }
        }

Then set RecyclerVIew Layout Manager as follow:

recyclerView.setLayoutManager(new CustomLinearLayoutManager(activity));

-----------------SOLUTION 3---------------

  • This solution fixes the issue if it caused by reason #3. You need to make sure that you are using the notify methods in the correct way. Alternatively, use DiffUtil to handle the change in a smart, easy, and smooth way. Using DiffUtil in Android RecyclerView

-----------------SOLUTION 4---------------

  • For reason #2, you need to check all data access to recycler list and make sure that there is no modification on another thread.
Islam Assi
  • 991
  • 1
  • 13
  • 18
  • this worked in my scenario, I can't use DiffUtil because I have custom components for recyclers and adapters, and the error occurs exactly in specific scenarios which are known, I just needed to patch it WITHOUT resorting to removing item animators, so I just wrapped it in a try&catch – usernotnull Jul 28 '19 at 07:37
  • For me it's not a threading issue, it's also not a notify adapter issue either because I am using DiffUtil. This error doesn't happen every single time, and sometimes randomly so it's very hard to reproduce. Adding ```recyclerView.setItemAnimator(null);``` to disable the DiffUtil animations seems to have caused no more crashes. Thanks for this answer. I was spending so much time trying to figure out why it's crashing. – DIRTY DAVE Apr 23 '23 at 05:12
21

I had a similar problem.

Problem in error code below:

int prevSize = messageListHistory.size();
// some insert
adapter.notifyItemRangeInserted(prevSize - 1, messageListHistory.size() -1);

Solution:

int prevSize = messageListHistory.size();
// some insert
adapter.notifyItemRangeInserted(prevSize, messageListHistory.size() -prevSize);
Willi Mentzel
  • 27,862
  • 20
  • 113
  • 121
Vandai Doan
  • 211
  • 2
  • 4
16

According to this issue, the problem has been resolved and was likely released some time near the beginning of 2015. A quote from that same thread:

It is specifically related to calling notifyDataSetChanged. [...]

Btw, I strongly advice not using notifyDataSetChanged because it kills animations and performance. Also for this case, using specific notify events will work around the issue.

If you are still having issues with a recent version of the support library, I would suggest reviewing your calls to notifyXXX (specifically, your use of notifyDataSetChanged) inside your adapter, to make sure you are adhering to the (somewhat delicate/obscure) RecyclerView.Adapter contract. Also be sure to issue those notifications on the main thread.

Community
  • 1
  • 1
stkent
  • 19,772
  • 14
  • 85
  • 111
  • 18
    not really, i agree with your part about performance but notifyDataSetChanged() does not kill animations, to animate using notifyDataSetChanged(), a) call setHasStableIds(true) on your RecyclerView.Adapter object and b) override getItemId inside your Adapter to return a unique long value for each row and check it out, the animations do work – PirateApp Nov 18 '15 at 10:51
  • @PirateApp You should consider making your comment as an answer. I have tried it and it's working fine. – mr5 May 30 '18 at 09:45
  • Not true! Still get reports from Google Console about this issue. And device of course is Samsung - `Samsung Galaxy J3(2017) (j3y17lte), Android 8.0 ` – user25 Jan 05 '19 at 23:19
13

I had the same problem. It was caused because I delayed notification for adapter about item insert.

But ViewHolder tried to redraw some data in it's view and it started the RecyclerView measuring and recounting children count - at that moment it crashed (items list and it's size was already updated, but the adapter was not notified yet).

Willi Mentzel
  • 27,862
  • 20
  • 113
  • 121
porfirion
  • 1,619
  • 1
  • 16
  • 23
9

another reason this problem happens is when you call these methods with wrong indexes (indexes which there has NOT happened insert or remove in them)

-notifyItemRangeRemoved

-notifyItemRemoved

-notifyItemRangeInserted

-notifyItemInserted

check indexe parameters to these methods and make sure they are precise and correct.

Amir Ziarati
  • 14,248
  • 11
  • 47
  • 52
9

In my case, I was getting this problem because of getting data updates from server (I am using Firebase Firestore) and while the first set of data is being processed by DiffUtil in the background, another set of data update comes and causes a concurrency issue by starting another DiffUtil.

In short, if you are using DiffUtil on a Background thread which then comes back to the Main Thread to dispatch the results to the RecylerView, then you run the chance of getting this error when multiple data updates come in short time.

I solved this by following the advice in this wonderful explanation: https://medium.com/@jonfhancock/get-threading-right-with-diffutil-423378e126d2

Just to explain the solution is to push the updates while the current one is running to a Deque. The deque can then run the pending updates once the current one finishes, hence handling all subsequent updates but avoiding inconsistency errors as well!

Hope this helps because this one made me scratch my head!

dejavu89
  • 754
  • 1
  • 7
  • 17
8

This happens when you specify the incorrect position for the notifyItemChanged , notifyItemRangeInserted etc.For me :

Before : (Erroneous)

public void addData(List<ChannelItem> list) {
  int initialSize = list.size();
  mChannelItemList.addAll(list);
  notifyItemRangeChanged(initialSize - 1, mChannelItemList.size());
 } 

After : (Correct)

 public void addData(List<ChannelItem> list) {
  int initialSize = mChannelItemList.size();
  mChannelItemList.addAll(list);
  notifyItemRangeInserted(initialSize, mChannelItemList.size()-1); //Correct position 
 }
Saurabh Padwekar
  • 3,888
  • 1
  • 31
  • 37
  • 2
    Why `notifyItemRangeInserted(initialSize, mChannelItemList.size()-1);` and not `notifyItemRangeInserted(initialSize, list.size());`? – CoolMind Jul 02 '19 at 12:25
  • Undestood. You mixed up `initialSize` and `list` size. So, both of your variants are wrong. – CoolMind Jul 23 '19 at 11:55
  • For me it works with `notifyItemRangeInserted(initialSize, list.size()-1);` but I don't get it. Why do I have to reduce the inserted size by one for the itemCount? – plexus Oct 16 '19 at 09:23
6

This bug is still not fixed in 23.1.1, but a common workaround would be to catch the exception.

Farooq AR
  • 314
  • 4
  • 9
5

In my case every time when I call notifyItemRemoved(0), it crashed. Turned out that I set setHasStableIds(true) and in getItemId I just returned the item position. I ended out updating it to return the item's hashCode() or self-defined unique id, which solved the issue.

Arst
  • 3,098
  • 1
  • 35
  • 42
4

This problem is caused by RecyclerView Data modified in different thread

Can confirm threading as one problem and since I ran into the issue and RxJava is becoming increasingly popular: make sure that you are using .observeOn(AndroidSchedulers.mainThread()) whenever you're calling notify[whatever changed]

code example from adapter:

myAuxDataStructure.getChangeObservable().observeOn(AndroidSchedulers.mainThread()).subscribe(new Observer<AuxDataStructure>() {

    [...]

    @Override
    public void onNext(AuxDataStructure o) {
        [notify here]
    }
});
Philipp
  • 433
  • 3
  • 14
  • I am on main Thread while calling DiffUtil.calculateDiff(diffUtilForecastItemChangesAnlayser(this.mWeatherForecatsItemWithMainAndWeathers, weatherForecastItems)).dispatchUpdatesTo(this); the log is clear onThread:Thread[main,5,main] – Mathias Seguy Android2ee Jul 03 '18 at 10:27
4

I ran into the same problem.

My app uses Navigation components with a fragment containing my recyclerView. My list displayed fine the first time the fragment was loaded ... but upon navigating away and coming back this error occurred.

When navigating away the fragment lifecycle went only through onDestroyView and upon returning it started at onCreateView. However, my adapter was initialized in the fragment's onCreate and did not reinitialize when returning.

The fix was to initialize the adapter in onCreateView.

Hope this may help someone.

Loren
  • 820
  • 2
  • 11
  • 18
4

Some times it is because of miss using of notifyItemRemoved and notifyItemInserted. Make sure for removing you use :

list.remove(position);
notifyItemRemoved(position);

and for adding item use :

list.add(position, item);
notifyItemInserted(position);

and at the end don't forget to :

notifyItemRangeChanged(position, list.size());
StackOverflower
  • 369
  • 2
  • 12
3

The error can be caused by your changes being inconsistent with what you are notifying. In my case:

myList.set(position, newItem);
notifyItemInserted(position);

What I of course had to do:

myList.add(position, newItem);
notifyItemInserted(position);
Pang
  • 9,564
  • 146
  • 81
  • 122
Cristan
  • 12,083
  • 7
  • 65
  • 69
3

Problem occured for me only when:

I created the Adapter with an empty list. Then I inserted items and called notifyItemRangeInserted.

Solution:

I solved this by creating the Adapter only after I have the first chunk of data and initialzing it with it right away. The next chunk could then be inserted and notifyItemRangeInserted called with no problem .

Willi Mentzel
  • 27,862
  • 20
  • 113
  • 121
  • I don't think it is the reason. I have many adapters with empty lists, then added items with `notifyItemRangeInserted`, but never had this exception there. – CoolMind Jul 24 '19 at 12:07
3

My problem was that even though i clear both the array list containing the data model for the recycler view, i did not notify the adapter of that change, so it had stale data from previous model. Which caused the confusion about the view holder position. To Fix this always notify the adapter that the data-set as changed before updating again.

Remario
  • 3,813
  • 2
  • 18
  • 25
3

In my case I was changing the data previously inside a thread with mRecyclerView.post(new Runnable...) and then again later changed data in the UI thread, which caused inconsistency.

Niroj Shr
  • 157
  • 13
3

I ran into the same problem when I have both removed and updated items in the list... After days of investigating I think I finally found a solution.

What you need to do is first do all the notifyItemChanged of your list and only then do all the notifyItemRemoved in a descending order

I hope this will help people that are running into the same issue...

Talihawk
  • 1,299
  • 9
  • 18
3

Solved for me by updating the recycler view to the last version

implementation "androidx.recyclerview:recyclerview:1.2.1"
Muhammad Helmi
  • 395
  • 2
  • 10
2

In my case the problem was that I used notifyDataSetChanged when amount of newly loaded data was less than initial data. This approach helped me:

adapter.notifyItemRangeChanged(0, newAmountOfData + 1);
adapter.notifyItemRangeRemoved(newAmountOfData + 1, previousAmountOfData);
2

Thanks to @Bolling idea I have implement to support to avoid any nullable from List

public void setList(ArrayList<ThisIsAdapterListObject> _newList) {
    //get the current items
    if (ThisIsAdapterList != null) {
        int currentSize = ThisIsAdapterList.size();
        ThisIsAdapterList.clear();
        //tell the recycler view that all the old items are gone
        notifyItemRangeRemoved(0, currentSize);
    }

    if (_newList != null) {
        if (ThisIsAdapterList == null) {
            ThisIsAdapterList = new ArrayList<ThisIsAdapterListObject>();
        }
        ThisIsAdapterList.addAll(_newList);
        //tell the recycler view how many new items we added
        notifyItemRangeInserted(0, _newList.size());
    }
}
Sruit A.Suk
  • 7,073
  • 7
  • 61
  • 71
1

In my case I've had more then 5000 items in the list. My problem was that when scrolling the recycler view, sometimes the "onBindViewHolder" get called while "myCustomAddItems" method is altering the list.

My solution was to add "synchronized (syncObject){}" to all the methods that alter the data list. This way at any point at time only one method can read this list.

user3193413
  • 575
  • 7
  • 10
1

I am using a Cursor so I can not use the DiffUtils as proposed in the popular answers. In order to make it work for me I am disabling animations when the list is not idle. This is the extension that fixes this issue:

 fun RecyclerView.executeSafely(func : () -> Unit) {
        if (scrollState != RecyclerView.SCROLL_STATE_IDLE) {
            val animator = itemAnimator
            itemAnimator = null
            func()
            itemAnimator = animator
        } else {
            func()
        }
    }

Then you can update your adapter like that

list.executeSafely {
  adapter.updateICursor(newCursor)
}
Micer
  • 8,731
  • 3
  • 79
  • 73
joecks
  • 4,539
  • 37
  • 48
1

To me it happened with ObservableList.removeIf.

removeIf(predicate) is misimplemented: it doesn't send removal notifications. So the indices were obviously wrong, as elements were removed without RV knowing.

The correct method is removeAll(predicate).

Agent_L
  • 4,960
  • 28
  • 30
0

I got this error because I was mistakenly calling a method to remove a specific row from my recyclerview multiple times. I had a method like:

void removeFriends() {
    final int loc = data.indexOf(friendsView);
    data.remove(friendsView);
    notifyItemRemoved(loc);
}

I was accidentally calling this method three times instead of once, so the second time loc was -1 and the error was given when it tried to remove it. The two fixes were to ensure the method was only called once, and also to add a sanity check like this:

void removeFriends() {
    final int loc = data.indexOf(friendsView);
    if (loc > -1) {
        data.remove(friendsView);
        notifyItemRemoved(loc);
    }
}
elliptic1
  • 1,654
  • 1
  • 19
  • 22
0

I got the same problem and I have read that this happened in Samsung phones only...But the reality showed that this happens in a lot of brands.

After testing I realized that this happens only when you scroll fast the RecyclerView and then you go back either with the back button or the Up button. So I put inside Up button and onBackpressed the below snippet:

someList = new ArrayList<>();
mainRecyclerViewAdapter = new MainRecyclerViewAdapter(this, someList, this);
recyclerViewMain.setAdapter(mainRecyclerViewAdapter);
finish();

With this solution you just load a new Arraylist to the adapter and new adapter to recyclerView and then you finish activity.

Hope it helps someone

Farmaker
  • 2,700
  • 11
  • 16
0

I got this error because i was calling "notifyItemInserted" twice by mistake.

Feuby
  • 708
  • 4
  • 7
0

In my case, adapter data changed. And i was wrongly use notifyItemInserted() for these changes. When i use notifyItemChanged, the error has gone away.

oiyio
  • 5,219
  • 4
  • 42
  • 54
0

I ran into the same problem when I have update data while the RecyclerView is scrolling. And I fixed it with the following solution:

  1. Stop scroll our RecyclerView before update data.
  2. Custom layout manager likes with the preceding answer.
  3. Use DiffUtils to ensure updating data is correct.
nguyencse
  • 1,028
  • 9
  • 17
0

If anyone still facing this issue, Try using RecyclerView inside Relative/LinearLayout inside NestedScrollView inside CoordinatorLayout

yashagl
  • 11
  • 4
0

I was getting this issue, after 1 hour of investigation I couldn't find the root cause then migrated from RecyclerView.Adapter to [ListAdapter][1] with DiffUtil callback

Sumit
  • 1,022
  • 13
  • 19
-1

If your data changes a lot, you can use

 mAdapter.notifyItemRangeChanged(0, yourData.size());

or some single items in your data set changes, your can use

 mAdapter.notifyItemChanged(pos);

For detailed methods usage, you can refer the doc, in a way, try not to directly use mAdapter.notifyDataSetChanged().

davejal
  • 6,009
  • 10
  • 39
  • 82
Arron Cao
  • 416
  • 2
  • 9
  • 2
    using `notifyItemRangeChanged` also produces the same crash. – lionelmessi Jan 11 '16 at 04:10
  • That suits some situation. Maybe you updated your dataset both in background thread and UI thread, this will also cause inconsistency. If you only update the dataset in UI thread, it will work. – Arron Cao Jan 15 '16 at 21:29