RecyclerView и java.lang.IndexOutOfBoundsException: обнаружено несоответствие. Неправильное положение держателя видового держателя ViewHolder в устройствах Samsung

254

У меня есть вид переработчика, который отлично работает на всех устройствах, кроме Samsung. На самсунг я получаю

java.lang.IndexOutOfBoundsException: обнаружено несоответствие. Неправильный вид держателя адаптера positionViewHolder

когда я вернусь к фрагменту с видом переработчика из другого действия.

Код адаптера:

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

Исключение:

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)

Как я могу это исправить?

Владимир Фишер
источник
когда вы вернетесь, ваши данные такие же, как когда вы покидаете страницу?
хусрав
я решаю ту же проблему, как вы решаете ....
Ашвин Соланки
@ Владимир Вы нашли окончательный ответ?
Алиреза Нурали
В моем случае это было потому, что я приступил к асинхронной задаче, и когда один из них завершается раньше другого, а пользователь выполняет прокрутку вниз, а тем временем другой пользователь завершает и обновляет адаптер, пользователь может получить такое исключение, потому что вторая задача вернула меньшее количество данных
Vasif

Ответы:

196

Эта проблема вызвана RecyclerViewизменением данных в другом потоке. Лучший способ - это проверка доступа к данным. И это обходной путь LinearLayoutManager.

Предыдущий ответ

На самом деле была ошибка в RecyclerView, и поддержка 23.1.1 все еще не исправлена.

Для обходного пути, обратите внимание, что стеки трассировки, если мы можем поймать это Exceptionв одном из классов, могут пропустить этот сбой. Для меня я создаю LinearLayoutManagerWrapperи переопределяю 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");
        }
    }
}

Затем установите его на RecyclerView:

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

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

На самом деле поймать это исключение, и, похоже, никаких побочных эффектов еще нет.

Кроме того, если вы используете GridLayoutManagerили StaggeredGridLayoutManagerвы должны создать обертку для него.

Примечание: RecyclerViewможет быть в неправильном внутреннем состоянии.

Sakim
источник
1
где именно ты положил это? на адаптер или активность?
Стив Камау
расширить LinearLayoutManagerи переопределить это. Я буду дополнением в своем ответе.
СакиМ
14
code.google.com/p/android/issues/detail?id=158046 ответ № 12 сказал: не делай этого.
Роберт
ммм, вы правы Кажется, трудно обезвредить все потенциальные изменения, не связанные с пользовательским интерфейсом, в моем приложении, я оставлю это только в качестве обходного пути.
СакиМ
1
Для моего случая я делаю в той же теме. . mDataHolder.get () RemoveAll (mHiddenGenre); mAdapter.notifyItemRangeRemoved (mExpandButtonPosition, mHiddenGenre.size ());
JehandadK
73

Это пример обновления данных с совершенно новым контентом. Вы можете легко изменить его в соответствии с вашими потребностями. Я решил это в моем случае, позвонив:

notifyItemRangeRemoved(0, previousContentSize);

перед:

notifyItemRangeInserted(0, newContentSize);

Это правильное решение и также упоминается в этом посте участником проекта AOSP.

коробка
источник
2
Это решение работает для меня, я попробовал много ответов здесь, но они не работают (я не тестировал первое решение, Thaugh)
AndroLife
Проблема заключается в том, что использование этих методов создает эту несогласованность, даже если это делается в одном потоке.
JehandadK
Я не пользуюсь notifyItemRangeInsertedи имею эту проблему с некоторыми устройствами Samsung
user25
И совсем не по теме здесь. Автор не использовалnotifyItemRangeInserted
user25
1
Спасибо! Это помогло мне.
Дмитрий Канунниковофф
35

Однажды я столкнулся с этой проблемой, и решил ее, обернув LayoutManagerи отключив прогностическую анимацию.

Вот пример:

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

И установите это RecyclerView:

RecyclerView.LayoutManager mLayoutManager = new LinearLayoutManagerWrapper(context, LinearLayoutManager.VERTICAL, false);
hcknl
источник
Кажется, это работает для меня, но вы можете сказать, почему это работает?
Деннис Андерсон
Исправлено для меня тоже. Как вы предсказали, что это может быть причиной этого сбоя.
Рахул Растоги
1
Базовый класс метода LinearLayoutManager supportPredictiveAnimations () возвращает false по умолчанию. Что мы получаем, переопределяя метод здесь? public boolean supportsPredictiveItemAnimations() { return false; }
М. Хиг
1
@ M.Hig Документация для LinearLayoutManagerговорит, что по умолчанию ложно, но это утверждение ложно :-( Декомпилированный код LinearLayoutManagerимеет следующее: public boolean supportsPredictiveItemAnimations () {return this.mPendingSavedState == null && this.mLastStackFromEnd == this.mStackFromEnd ;}
Клайд
Я использую утилиты diff для обновления моего адаптера представления переработчика, и этот ответ исправил сбой. Большое спасибо, дорогой автор!
Евгений П.
29

Новый ответ: используйте DiffUtil для всех обновлений RecyclerView. Это поможет как с производительностью, так и с ошибкой выше. Посмотреть здесь

Предыдущий ответ: это сработало для меня. Ключ заключается в том, чтобы не использовать notifyDataSetChanged()и делать правильные вещи в правильном порядке:

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
источник
1
Это самое тщательное решение с хорошим объяснением. Спасибо!
Сакибой
тогда какова цель использования notifyitemrangeinted вместо notifydatasetchanged (), @Bolling.
Ankur_009
@FilipLuch Можете ли вы объяснить, почему?
Sreekanth Karumanaghat
3
@SreekanthKarumanagh, конечно, не знаю, почему я не объяснил причину. В основном он очищает, а затем воссоздает все элементы в списке. Как и в результатах поиска, очень часто вы получаете одни и те же элементы, или когда выполняется обновление, вы получаете одни и те же элементы, а затем вы в конечном итоге воссоздаете все, что является пустой тратой производительности. Вместо этого используйте DiffUtils и обновляйте только изменения, а не все элементы. Это все равно, что каждый раз переходить от А к Я, но там вы только изменили F.
Филипп Лучианенко
2
DiffUtil - это скрытое сокровище. Спасибо, что поделился!
Sileria
22

Причины вызвали эту проблему:

  1. Внутренняя проблема в Recycler, когда анимации элементов включены
  2. Модификация данных Recycler в другом потоке
  3. Вызов методов уведомления неправильно

РЕШЕНИЕ:

----------------- РЕШЕНИЕ 1 ---------------

  • Поймать исключение (не рекомендуется, особенно по причине № 3)

Создайте пользовательский LinearLayoutManager, как показано ниже, и установите его в 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");
                }

            }
        }

Затем установите RecyclerVIew Layout Manager следующим образом:

recyclerView.setLayoutManager(new CustomLinearLayoutManager(activity));

----------------- РЕШЕНИЕ 2 ---------------

  • Отключить анимацию элементов (исправляет проблему, если она вызвана причиной № 1):

Опять же, создайте собственный менеджер линейного макета следующим образом:

    public class CustomLinearLayoutManager extends LinearLayoutManager {

            //Generate constructors

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

Затем установите RecyclerVIew Layout Manager следующим образом:

recyclerView.setLayoutManager(new CustomLinearLayoutManager(activity));

----------------- РЕШЕНИЕ 3 ---------------

  • Это решение устраняет проблему, если она вызвана причиной № 3. Вы должны убедиться, что вы используете методы уведомления правильно. В качестве альтернативы используйте DiffUtil, чтобы обрабатывать изменения разумным, простым и плавным способом. Использование DiffUtil в Android RecyclerView

----------------- РЕШЕНИЕ 4 ---------------

  • По причине №2 вам необходимо проверить доступ всех данных к списку переработчиков и убедиться, что в другом потоке нет изменений.
Ислам Асси
источник
это работало в моем сценарии, я не могу использовать DiffUtil, потому что у меня есть пользовательские компоненты для рециркуляторов и адаптеров, и ошибка возникает именно в определенных известных сценариях, мне просто нужно было ее исправить, БЕЗ обращения к удалению аниматоров элементов, поэтому я просто завернул его в попытку и поймать
RJFares
17

У меня была похожая проблема.

Проблема в коде ошибки ниже:

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

Решение:

int prevSize = messageListHistory.size();
// some insert
adapter.notifyItemRangeInserted(prevSize, messageListHistory.size() -prevSize);
Вандай Доан
источник
Это отлично сработало для меня! Не уверен, почему мы не можем просто использовать, newList.size() - 1хотя.
Waseefakhtar
15

Согласно этой проблеме , проблема была решена и, вероятно, была выпущена в начале 2015 года . Цитата из той же ветки :

Это конкретно связано с вызовом notifyDataSetChanged. [...]

Кстати, я настоятельно советую не использовать notifyDataSetChanged, потому что это убивает анимацию и производительность. Также для этого случая использование определенных событий уведомления будет работать вокруг проблемы.

Если у вас все еще возникают проблемы с последней версией библиотеки поддержки, я бы посоветовал проверить ваши звонки notifyXXX(в частности, ваше использование notifyDataSetChanged) внутри вашего адаптера, чтобы убедиться, что вы придерживаетесь (несколько деликатного / неясного) RecyclerView.Adapterконтракта. Также обязательно отправляйте эти уведомления в основной поток.

stkent
источник
16
не совсем, я согласен с вашей стороны по поводу производительности, но notifyDataSetChanged () не убивает анимации, чтобы анимировать с помощью notifyDataSetChanged (), a) вызов setHasStableIds (true) для вашего объекта RecyclerView.Adapter и б) переопределить getItemId внутри вашего адаптера, чтобы вернуть уникальное длинное значение для каждой строки и посмотрите, анимации работают
PirateApp
@PirateApp Вы должны рассмотреть возможность сделать свой комментарий в качестве ответа. Я попробовал, и он работает нормально.
mr5
Не правда! Тем не менее получать отчеты из консоли Google об этой проблеме. И устройство, конечно же, Samsung -Samsung Galaxy J3(2017) (j3y17lte), Android 8.0
user25
10

У меня такая же проблема. Это было вызвано тем, что я отложил уведомление адаптера о вставке элемента.

Но ViewHolder попытался перерисовать некоторые данные в своем представлении, и он начал RecyclerViewизмерять и пересчитывать количество детей - в тот момент он падал (список элементов и его размер уже были обновлены, но адаптер еще не был уведомлен).

porfirion
источник
8

Это происходит, когда вы указываете неправильную позицию для notifyItemChanged, notifyItemRangeInserted и т. Д. Для меня:

До: (ошибочно)

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

После: (исправить)

 public void addData(List<ChannelItem> list) {
  int initialSize = mChannelItemList.size();
  mChannelItemList.addAll(list);
  notifyItemRangeInserted(initialSize, mChannelItemList.size()-1); //Correct position 
 }
Саурабх Падвекар
источник
1
Почему notifyItemRangeInserted(initialSize, mChannelItemList.size()-1);и нет notifyItemRangeInserted(initialSize, list.size());?
CoolMind
Undestood. Вы перепутали initialSizeи listразмер. Итак, оба ваших варианта неверны.
CoolMind
Для меня это работает, notifyItemRangeInserted(initialSize, list.size()-1);но я не понимаю. Почему я должен уменьшить вставленный размер на единицу для itemCount?
сплетение
7

Еще одна причина, по которой эта проблема возникает, - когда вы вызываете эти методы с неправильными индексами (индексы, которых там НЕ произошло, вставляют или удаляют в них)

-notifyItemRangeRemoved

-notifyItemRemoved

-notifyItemRangeInserted

-notifyItemInserted

проверьте параметры indexe для этих методов и убедитесь, что они точные и правильные.

Амир Зиарати
источник
2
Это была моя проблема. Исключение происходит, когда ничего не добавляется в список.
Rasel
6

Эта ошибка все еще не исправлена ​​в 23.1.1, но общий обходной путь должен был бы поймать исключение.

Фарук А.Р.
источник
19
Поймай это где именно? Единственный код в трассировке стека - это собственный код Android.
howettl
1
Поймай это как ответ @saki_M.
Ренан Бандейра
Это на самом деле работает, хотя для вас, хотя Ренан? Вы проверяли исправление некоторое время? Ошибка возникает только изредка, поэтому я только посмотрю, работает ли она со временем.
Симон
Это на самом деле работает, но некоторые детские взгляды в моем случае остаются без остатка.
Дэвид
@ david Непоследовательно остается, каковы последствия этого?
Sreekanth Karumanaghat
4

Эта проблема вызвана изменением данных RecyclerView в другом потоке

Можно подтвердить многопоточность как одну проблему, и поскольку я столкнулся с проблемой, и RxJava становится все более популярным: убедитесь, что вы используете его, .observeOn(AndroidSchedulers.mainThread())когда звонитеnotify[whatever changed]

Пример кода из адаптера:

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

    [...]

    @Override
    public void onNext(AuxDataStructure o) {
        [notify here]
    }
});
Philipp
источник
Я нахожусь в главном потоке при вызове DiffUtil.calculateDiff (diffUtilForecastItemChangesAnlayser (this.mWeatherForecatsItemWithMainAndWeathers, weatherForecastItems)). DispatchUpdatesTo (this); лог понятен на темы: Тема [главная, 5, основная]
Матиас Сегу Android2ee
4

В моем случае каждый раз, когда я вызываю notifyItemRemoved (0), происходит сбой. Оказалось, что я установил setHasStableIds(true)и getItemIdтолько что вернул позицию предмета. В итоге я обновил его, чтобы он возвращал hashCode()уникальный идентификатор элемента или самоопределяемый код, что решило проблему.

Arst
источник
4

В моем случае я столкнулся с этой проблемой из-за получения обновлений данных с сервера (я использую Firebase Firestore), и в то время как первый набор данных обрабатывается DiffUtil в фоновом режиме, другой набор данных обновляется и вызывает проблему параллелизма. запустив еще один DiffUtil.

Короче говоря, если вы используете DiffUtil в фоновом потоке, который затем возвращается в основной поток для отправки результатов в RecylerView, то у вас есть шанс получить эту ошибку, когда несколько обновлений данных происходят в короткие сроки.

Я решил это, следуя совету в этом замечательном объяснении: https://medium.com/@jonfhancock/get-threading-right-with-diffutil-423378e126d2

Просто для того, чтобы объяснить, что решение состоит в том, чтобы отправить обновления, пока текущее работает в Deque. Затем deque может запускать ожидающие обновления после завершения текущего, следовательно, обрабатывая все последующие обновления, но также избегая ошибок несогласованности!

Надеюсь, это поможет, потому что этот заставил меня почесать голову!

dejavu89
источник
Спасибо за ссылку!
CoolMind
3

Проблема возникла у меня только тогда, когда:

Я создал Адаптер с пустым списком . Затем я вставил предметы и позвонил notifyItemRangeInserted.

Решение:

Я решил это, создав адаптер только после того, как у меня есть первый кусок данных и сразу его инициализировал. Следующий блок может быть вставлен и notifyItemRangeInsertedвызван без проблем.

Вилли Ментцель
источник
Я не думаю, что это причина. У меня есть много адаптеров с пустыми списками, затем добавлены элементы notifyItemRangeInserted, но такого исключения там никогда не было.
CoolMind
3

Моя проблема заключалась в том, что, хотя я очищал оба списка массивов, содержащих модель данных для представления переработчика, я не уведомлял адаптер об этом изменении, поэтому он имел устаревшие данные из предыдущей модели. Что вызвало недоразумение относительно позиции держателя вида. Чтобы исправить это, всегда сообщайте адаптеру, что набор данных изменился, прежде чем обновлять снова.

Remario
источник
или просто уведомить, если предмет удален вместо этого
Remario
моя модель использует ссылку на контейнер, поэтому
Remario
3

В моем случае я изменял данные, ранее находившиеся в потоке, с помощью mRecyclerView.post (новый Runnable ...), а затем снова изменял данные в потоке пользовательского интерфейса, что вызывало несогласованность.

Нирой Шр
источник
1
У меня такая же ситуация, как и у вас, как вы ее решили? спасибо
бадерхане
2

Ошибка может быть вызвана тем, что ваши изменения не соответствуют тому, что вы уведомляете. В моем случае:

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

Что я, конечно, должен был сделать:

myList.add(position, newItem);
notifyItemInserted(position);
Cristan
источник
2

В моем случае проблема заключалась в том, что я использовал notifyDataSetChanged, когда количество вновь загруженных данных было меньше, чем исходные данные. Этот подход помог мне:

adapter.notifyItemRangeChanged(0, newAmountOfData + 1);
adapter.notifyItemRangeRemoved(newAmountOfData + 1, previousAmountOfData);
pretty_fennec
источник
Почему notifyDataSetChangedзависит от новых данных? Я думал, что это обновит весь список.
CoolMind
2

Я столкнулся с той же проблемой.

Мое приложение использует компоненты навигации с фрагментом, содержащим мой recyclerView. Мой список отображался нормально при первой загрузке фрагмента ... но при удалении и возвращении произошла эта ошибка.

При перемещении по фрагменту жизненный цикл фрагмента проходил только через onDestroyView, а по возвращении он начинался с onCreateView. Однако, мой адаптер был инициализирован в onCreate фрагмента и не был повторно инициализирован при возврате.

Исправление состояло в том, чтобы инициализировать адаптер в onCreateView.

Надеюсь, что это может кому-то помочь.

Loren
источник
0

Я получил эту ошибку, потому что я несколько раз ошибочно вызывал метод для удаления определенной строки из моего окна повторного использования. У меня был метод как:

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

Я случайно вызывал этот метод три раза вместо одного, поэтому второй раз locбыл равен -1, и при попытке удалить его была выдана ошибка. Эти два исправления должны были гарантировать, что метод был вызван только один раз, а также добавить проверку работоспособности следующим образом:

void removeFriends() {
    final int loc = data.indexOf(friendsView);
    if (loc > -1) {
        data.remove(friendsView);
        notifyItemRemoved(loc);
    }
}
elliptic1
источник
0

У меня возникла та же проблема, и я прочитал, что это произошло только в телефонах Samsung ... Но реальность показала, что это происходит во многих брендах.

После тестирования я понял, что это происходит только тогда, когда вы быстро прокручиваете RecyclerView и затем возвращаетесь либо с помощью кнопки «Назад», либо с помощью кнопки «Вверх». Поэтому я поместил внутри кнопку «Вверх» и нажал на кнопку нижеприведенного фрагмента:

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

С этим решением вы просто загружаете новый Arraylist в адаптер и новый адаптер в recyclerView, а затем завершаете работу.

Надеюсь, это поможет кому-то

Farmaker
источник
0

Я получил эту ошибку, потому что я дважды вызывал «notifyItemInserted» по ошибке.

Feuby
источник
0

В моем случае в списке было более 5000 наименований. Моя проблема заключалась в том, что при прокрутке представления рециркулятора иногда вызывается «onBindViewHolder», а метод «myCustomAddItems» изменяет список.

Мое решение состояло в том, чтобы добавить «synchronized (syncObject) {}» ко всем методам, которые изменяют список данных. Таким образом, в любой момент времени только один метод может прочитать этот список.

user3193413
источник
0

В моем случае данные адаптера изменились. И я был неправильно использовать notifyItemInserted () для этих изменений. Когда я использую notifyItemChanged, ошибка исчезла.

oiyio
источник
0

Я столкнулся с той же проблемой, когда удалял и обновлял элементы в списке ... После нескольких дней исследований я думаю, что наконец нашел решение.

То, что вам нужно сделать, это сначала сделать все notifyItemChangedиз вашего списка и только потом делать все notifyItemRemoved в порядке убывания

Я надеюсь, что это поможет людям, которые сталкиваются с той же проблемой ...

Talihawk
источник
0

Я использую Курсор, поэтому я не могу использовать DiffUtils, как предложено в популярных ответах. Чтобы это работало для меня, я отключаю анимацию, когда список не простаивает. Это расширение, которое решает эту проблему:

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

Затем вы можете обновить свой адаптер, как это

list.executeSafely {
  adapter.updateICursor(newCursor)
}
joecks
источник
0

Если проблема возникает после мультитач, вы можете отключить мультитач с

android:splitMotionEvents="false" 

в файле макета.

внутри
источник
-1

Если ваши данные сильно меняются, вы можете использовать

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

или некоторые отдельные элементы в вашем наборе данных изменяются, вы можете использовать

 mAdapter.notifyItemChanged(pos);

Для подробного использования методов вы можете обратиться к документу , пытаясь не использовать его напрямую mAdapter.notifyDataSetChanged().

Аррон Цао
источник
2
использование notifyItemRangeChangedтакже вызывает тот же сбой.
lionelmessi
Это подходит для некоторой ситуации. Возможно, вы обновили свой набор данных как в фоновом потоке, так и в потоке пользовательского интерфейса, это также приведет к несогласованности. Если вы обновите только набор данных в потоке пользовательского интерфейса, он будет работать.
Аррон Цао