RecyclerView i java.lang.IndexOutOfBoundsException: Wykryto niespójność. Nieprawidłowa pozycja adaptera uchwytu widoku ViewHolder w urządzeniach Samsung


253

Mam widok recyklera, który działa idealnie na wszystkich urządzeniach oprócz Samsunga. Na Samsunga dostaję

java.lang.IndexOutOfBoundsException: Wykryto niespójność. Nieprawidłowa pozycja adaptera uchwytu widoku ViewHolder

kiedy wracam do fragmentu z widokiem recyklera z innej działalności.

Kod adaptera:

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

Wyjątek:

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)

Jak mogę to naprawić?


kiedy wrócisz, czy Twoje dane są takie same jak po opuszczeniu strony?
khusrav

Zajmuję się tym samym problemem, w jaki sposób rozwiązać ....
Ashvin solanki

@ Владимир Czy znalazłeś ostateczną odpowiedź?
Alireza Noorali,

W moim przypadku było to spowodowane tym, że rozpocząłem zadanie asynchroniczne, a gdy jedno z nich zakończy się przed drugim, a użytkownik przewinie w dół, a tymczasem inny zakończy i zaktualizuje, użytkownik adaptera może uzyskać taki wyjątek, ponieważ drugie zadanie zwróciło mniej danych
Vasif

Odpowiedzi:


196

Przyczyną tego problemu są RecyclerViewDane zmodyfikowane w innym wątku. Najlepszym sposobem jest sprawdzenie dostępu do wszystkich danych. Obejście tego problemu się kończyLinearLayoutManager .

Poprzednia odpowiedź

W RecyclerView rzeczywiście wystąpił błąd, a obsługa 23.1.1 nadal nie została naprawiona.

Aby obejść ten problem, zwróć uwagę, że stosy śledzenia wstecznego mogą zostać przechwycone przez Exceptionjedną z klas, co może spowodować pominięcie tej awarii. Dla mnie tworzę LinearLayoutManagerWrapperi zastępuję 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");
        }
    }
}

Następnie ustaw na RecyclerView:

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

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

Właściwie złap ten wyjątek i wydaje się, że nie ma jeszcze żadnych skutków ubocznych.

Ponadto, jeśli używasz GridLayoutManagerlubStaggeredGridLayoutManager musisz utworzyć opakowanie dla niego.

Uwaga: RecyclerViewMoże być w złym stanie wewnętrznym.


1
gdzie dokładnie to umieszczasz? na adapterze lub aktywności?
Steve Kamau

przedłużyć LinearLayoutManageri zastąpić to. Dodam do mojej odpowiedzi.
sakiM

14
code.google.com/p/android/issues/detail?id=158046 odpowiedź nr 12 powiedziała, że ​​nie rób tego.
Robert,

ummm, masz rację. Wydaje mi się, że trudno jest usunąć wszystkie potencjalne modyfikacje wątków innych niż interfejs użytkownika w mojej aplikacji, zachowam to tylko jako obejście.
sakiM

1
W moim przypadku robię na tym samym wątku. mDataHolder.get (). removeAll (mHiddenGenre); mAdapter.notifyItemRangeRemoved (mExpandButtonPosition, mHiddenGenre.size ());
JehandadK

73

To jest przykład odświeżania danych przy użyciu całkowicie nowej zawartości. Możesz go łatwo zmodyfikować, aby dopasować do swoich potrzeb. Rozwiązałem to w moim przypadku, dzwoniąc:

notifyItemRangeRemoved(0, previousContentSize);

przed:

notifyItemRangeInserted(0, newContentSize);

Jest to poprawne rozwiązanie, a także jest mowa w tym poście przez państwo projektu AOSP.


2
To rozwiązanie działa dla mnie Próbowałem tutaj wielu odpowiedzi, ale one nie działają (nie przetestowałem pierwszego rozwiązania thaugh)
AndroLife

Problem polega na tym, że użycie tych metod powoduje powstanie tej niespójności, nawet w przypadku wykonania tego samego wątku.
JehandadK

Nie używam notifyItemRangeInsertedi mam ten problem z niektórymi urządzeniami Samsung
25

I całkiem nie na temat tutaj. Autor nie używałnotifyItemRangeInserted
25

1
Dziękuję Ci! To mi pomogło.
DmitryKanunnikoff

35

Raz zmierzyłem się z tym problemem i rozwiązałem go, włączając LayoutManageri wyłączając animacje predykcyjne.

Oto przykład:

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

I ustaw na RecyclerView:

RecyclerView.LayoutManager mLayoutManager = new LinearLayoutManagerWrapper(context, LinearLayoutManager.VERTICAL, false);

wydaje mi się, że to działa, ale czy możesz powiedzieć, dlaczego to działa?
Dennis Anderson

Naprawiono także dla mnie. Jak przewidziałeś, że może to być przyczyną tej awarii.
Rahul Rastogi,

1
Klasa podstawowa metody LinearLayoutManager obsługuje PredictiveAnimations () domyślnie zwraca wartość false. Co otrzymujemy, zastępując tutaj metodę? public boolean supportsPredictiveItemAnimations() { return false; }
M. Hig

1
@ M.Hig Dokumentacja LinearLayoutManagermówi, że wartością domyślną jest fałsz, ale ta instrukcja jest fałszywa :-( Dekompilowany kod dla LinearLayoutManagerma to: public boolean obsługujePredictiveItemAnimations () {return this.mPendingSavedState == null && this.mLastStackFromEnd == this.mStackFromEnd ;}
Clyde

Używam różnych programów do aktualizacji mojego adaptera widoku recyklera i ta odpowiedź naprawiła awarię. Wielkie dzięki, drogi autorze!
Eugene P.

29

Nowa odpowiedź: użyj DiffUtil dla wszystkich aktualizacji RecyclerView. Pomoże to zarówno pod względem wydajności, jak i powyższego błędu. Spójrz tutaj

Poprzednia odpowiedź: To zadziałało dla mnie. Kluczem jest, aby nie używać notifyDataSetChanged()i robić właściwe rzeczy we właściwej kolejności:

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

1
To najdokładniejsze rozwiązanie z dobrym wyjaśnieniem. Dzięki!
Sakiboy,

to jaki jest cel używania notitemrangeinserted zamiast notifydatasetchanged (), @Bolling.
Ankur_009

@FilipLuch Czy możesz wyjaśnić, dlaczego?
Sreekanth Karumanaghat

3
@SreekanthKarumanagh pewnie, nie wiem, dlaczego nie wyjaśniłem powodu. Zasadniczo usuwa, a następnie odtwarza wszystkie elementy na liście. Podobnie jak w wynikach wyszukiwania, bardzo często dostajesz te same elementy, lub po zakończeniu odświeżania dostajesz te same elementy, a następnie odtwarzane jest wszystko, co jest stratą wydajności. Zamiast tego używaj DiffUtils i aktualizuj tylko zmiany, a nie wszystkie elementy. To jest jak przejście od A do Z za każdym razem, ale zmieniłeś tam tylko F.
Filip Luchianenco

2
DiffUtil to ukryty skarb. Dzięki za udostępnienie!
Sileria

22

Przyczyny spowodowały ten problem:

  1. Wewnętrzny problem w Recycler, gdy animacje przedmiotów są włączone
  2. Modyfikacja danych Recycler w innym wątku
  3. Wywołanie metod powiadomień w niewłaściwy sposób

ROZWIĄZANIE:

----------------- ROZWIĄZANIE 1 ---------------

  • Łapanie wyjątku (niezalecane szczególnie z powodu nr 3)

Utwórz niestandardowy menedżer LinearLayoutManager w następujący sposób i ustaw go na 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");
                }

            }
        }

Następnie ustaw RecyclerVIew Layout Manager w następujący sposób:

recyclerView.setLayoutManager(new CustomLinearLayoutManager(activity));

----------------- ROZWIĄZANIE 2 ---------------

  • Wyłącz animacje przedmiotów (rozwiązuje problem, jeśli powodował przyczynę de 1):

Ponownie utwórz niestandardowego menedżera układu liniowego w następujący sposób:

    public class CustomLinearLayoutManager extends LinearLayoutManager {

            //Generate constructors

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

Następnie ustaw RecyclerVIew Layout Manager w następujący sposób:

recyclerView.setLayoutManager(new CustomLinearLayoutManager(activity));

----------------- ROZWIĄZANIE 3 ---------------

  • To rozwiązanie rozwiązuje problem, jeśli jest spowodowany przyczyną 3. Musisz upewnić się, że używasz metod powiadomień we właściwy sposób. Możesz też użyć DiffUtil, aby obsłużyć zmianę w inteligentny, łatwy i płynny sposób. Korzystanie z DiffUtil w Android RecyclerView

----------------- ROZWIĄZANIE 4 ---------------

  • Z powodu nr 2 musisz sprawdzić dostęp do wszystkich danych do listy podmiotów zajmujących się recyklingiem i upewnić się, że nie ma modyfikacji w innym wątku.

to zadziałało w moim scenariuszu, nie mogę użyć DiffUtil, ponieważ mam niestandardowe komponenty do recyklerów i adapterów, a błąd występuje dokładnie w określonych znanych scenariuszach, po prostu musiałem go załatać BEZ uciekania się do usuwania animatorów przedmiotów, więc po prostu zawinął w try & catch
RJFares

17

Miałem podobny problem.

Problem z kodem błędu poniżej:

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

Rozwiązanie:

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

To zadziałało dla mnie świetnie! Nie jestem pewien, dlaczego nie możemy tak po prostu użyć newList.size() - 1.
waseefakhtar,

15

Według tego problemu problem został rozwiązany i prawdopodobnie został wydany jakiś czas na początku 2015 roku . Cytat z tego samego wątku :

Jest to szczególnie związane z wywoływaniem powiadomićDataSetChanged. [...]

Przy okazji zdecydowanie odradzam korzystanie z funkcji powiadomieńDataSetChanged, ponieważ zabija ona animacje i wydajność. Również w tym przypadku użycie określonych zdarzeń powiadomień obejdzie problem.

Jeśli nadal masz problemy z najnowszą wersją biblioteki wsparcia, sugeruję przejrzenie twoich połączeń notifyXXX(w szczególności użycia notifyDataSetChanged) wewnątrz adaptera, aby upewnić się, że przestrzegasz (nieco delikatnej / niejasnej) RecyclerView.Adapterumowy. Pamiętaj również, aby wydawać powiadomienia w głównym wątku.


16
nie do końca, zgadzam się z twoją częścią dotyczącą wydajności, ale powiadomienieataDataSetChanged () nie zabija animacji, aby animować za pomocą noticeDataSetChanged (), a) wywołaj setHasStableIds (true) na obiekcie RecyclerView.Adapter ib) zastąpić getItemId wewnątrz adaptera, aby zwrócić wyjątkowa długa wartość dla każdego wiersza i sprawdź, animacje działają
PirateApp

@PirateApp Rozważ komentarz jako odpowiedź. Próbowałem i działa dobrze.
mr5

Nie prawda! Nadal otrzymuj raporty z Google Console na ten temat. A urządzeniem jest oczywiście Samsung -Samsung Galaxy J3(2017) (j3y17lte), Android 8.0
user25

10

Miałem ten sam problem. Było to spowodowane opóźnionym powiadomieniem adaptera o wstawieniu elementu.

Ale ViewHolderspróbowałem przerysować niektóre dane w jego widoku i zaczęło się RecyclerViewmierzenie i liczenie dzieci - w tym momencie uległo awarii (lista elementów i rozmiar zostały już zaktualizowane, ale adapter nie został jeszcze powiadomiony).


8

Dzieje się tak, gdy podasz niepoprawną pozycję w powiadomieniuItemChanged, powiadomieniuItemRangeInserted itp. Dla mnie:

Przed: (błędne)

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

Po: (poprawnie)

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

1
Dlaczego notifyItemRangeInserted(initialSize, mChannelItemList.size()-1);nie notifyItemRangeInserted(initialSize, list.size());?
CoolMind

Niezrozumiany. Zmieszałeś się initialSizei listrozmiar. Oba warianty są błędne.
CoolMind

Dla mnie to działa, notifyItemRangeInserted(initialSize, list.size()-1);ale nie rozumiem. Dlaczego muszę zmniejszyć wstawiony rozmiar o jeden dla itemCount?
splot

7

innym powodem tego problemu jest wywoływanie tych metod z niewłaściwymi indeksami (indeksy, których NIE zdarzyło się, wstawiaj lub usuwaj w nich)

-notifyItemRangeRemoved

-notifyItemRemoved

-notifyItemRangeInserted

-notifyItemInserted

sprawdź parametry indexe tych metod i upewnij się, że są dokładne i poprawne.


2
To był mój problem. Występuje wyjątek podczas dodawania niczego do listy.
Rasel

6

Ten błąd nie został jeszcze naprawiony w 23.1.1, ale powszechnym obejściem jest wychwycenie wyjątku.


18
Złap to gdzie dokładnie? Jedynym kodem w stosie śledzenia jest natywny kod systemu Android.
howettl

1
Złap to tak jak odpowiedź @saki_M.
Renan Bandeira,

Czy to naprawdę działa dla ciebie przez Renan? Czy przez jakiś czas testowałeś poprawkę? Błąd występuje tylko sporadycznie, więc zobaczę tylko, czy to działa z czasem.
Simon

To faktycznie działa, ale niektóre poglądy dzieci w moim są niekonsekwentnie pozostają.
David

@david Niespójnie pozostaje, co jest tego konsekwencją?
Sreekanth Karumanaghat

4

Ten problem jest spowodowany przez RecyclerView Data zmodyfikowany w innym wątku

Potwierdza wątki jako jeden problem, a ponieważ natknąłem się na problem, a RxJava staje się coraz bardziej popularny: upewnij się, że używasz za .observeOn(AndroidSchedulers.mainThread())każdym razem, gdy dzwonisznotify[whatever changed]

przykładowy kod z adaptera:

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

    [...]

    @Override
    public void onNext(AuxDataStructure o) {
        [notify here]
    }
});

Jestem w głównym wątku podczas wywoływania DiffUtil.calculateDiff (diffUtilForecastItemChangesAnlayser (this.mWeatherForecatsItemWithMainAndWeathers, weatherForecastItems)). DispatchUpdatesTo (this); dziennik jest wyczyszczony na wątku: Wątek [główny, 5, główny]
Mathias Seguy Android2ee

4

W moim przypadku za każdym razem, gdy wywołuję powiadomieniaItemRemoved (0), zawieszał się. Okazało się, że ustawiłem setHasStableIds(true)i getItemIdwłaśnie zwróciłem pozycję przedmiotu. Skończyłem aktualizować go, aby zwrócić hashCode()unikatowy identyfikator produktu lub samodzielnie zdefiniowany identyfikator, co rozwiązało problem.


4

W moim przypadku występował ten problem z powodu pobierania aktualizacji danych z serwera (korzystam z Firebase Firestore) i podczas gdy pierwszy zestaw danych jest przetwarzany przez DiffUtil w tle, pojawia się inny zestaw aktualizacji danych i powoduje problem z współbieżnością uruchamiając kolejny DiffUtil.

Krótko mówiąc, jeśli używasz DiffUtil w wątku tła, który następnie wraca do głównego wątku, aby wysłać wyniki do RecylerView, wtedy masz szansę na uzyskanie tego błędu, gdy wiele aktualizacji danych nastąpi w krótkim czasie.

Rozwiązałem to, postępując zgodnie ze wskazówkami zawartymi w tym cudownym wyjaśnieniu: https://medium.com/@jonfhancock/get-threading-right-with-diffutil-423378e126d2

Wyjaśnienie rozwiązania polega na wypchnięciu aktualizacji, gdy bieżąca jest uruchomiona na Deque. Deque może następnie uruchomić oczekujące aktualizacje po zakończeniu bieżącej, a tym samym obsługiwać wszystkie kolejne aktualizacje, ale także unikać błędów niespójności!

Mam nadzieję, że to pomaga, ponieważ to zmusiło mnie do podrapania się w głowę!


Dzięki za link!
CoolMind

3

Problem wystąpił dla mnie tylko wtedy, gdy:

Stworzyłem adapter z pustą listą . Potem wstawiłem przedmioty i zadzwoniłem notifyItemRangeInserted.

Rozwiązanie:

Rozwiązałem to, tworząc adapter dopiero po tym, jak mam pierwszy fragment danych i od razu go zainicjowałem. Następną porcję można następnie notifyItemRangeInsertedbez problemu wstawić i wywołać .


Nie sądzę, że to jest powód. Mam wiele adapterów z pustymi listami, a następnie dodawałem elementy notifyItemRangeInserted, ale nigdy nie miałem tego wyjątku.
CoolMind,

3

Mój problem polegał na tym, że chociaż wyczyściłem zarówno listę macierzy zawierającą model danych dla widoku programu do recyklingu, nie powiadomiłem adaptera o tej zmianie, więc zawierały nieaktualne dane z poprzedniego modelu. Co spowodowało zamieszanie dotyczące pozycji uchwytu widoku. Aby to naprawić, należy zawsze powiadomić adapter o zmianie zestawu danych przed ponowną aktualizacją.


lub po prostu powiadom, jeśli zamiast tego element zostanie usunięty
Remario

mój model wykorzystuje odniesienie do kontenera, dlatego
Remario

3

W moim przypadku zmieniłem dane wcześniej w wątku za pomocą mRecyclerView.post (nowy Runnable ...), a następnie ponownie zmieniłem dane w wątku interfejsu użytkownika, co spowodowało niespójność.


1
mam taką samą sytuację jak ty, jak to rozwiązałeś? dzięki
baderkhane

2

Błąd może być spowodowany niezgodnością wprowadzanych zmian z tym, co powiadamiasz. W moim przypadku:

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

Co oczywiście musiałem zrobić:

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

2

W moim przypadku problem polegał na tym, że użyłem powiadomieniaDataSetChanged, gdy ilość nowo załadowanych danych była mniejsza niż dane początkowe. To podejście pomogło mi:

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

Dlaczego notifyDataSetChangedzależy od nowych danych? Myślałem, że odświeży całą listę.
CoolMind,

2

Natrafiłem na ten sam problem.

Moja aplikacja korzysta ze składników Nawigacji z fragmentem zawierającym mój recyklerView. Moja lista wyświetlała się dobrze przy pierwszym załadowaniu fragmentu ... ale po opuszczeniu i powrocie wystąpił błąd.

Podczas nawigowania cykl życia fragmentu przebiegał tylko przez onDestroyView, a po zwróceniu zaczął się w onCreateView. Jednak mój adapter został zainicjowany w elemencie onCreate tego fragmentu i nie został ponownie zainicjowany podczas powrotu.

Rozwiązaniem była inicjalizacja adaptera w onCreateView.

Mam nadzieję, że to może komuś pomóc.


0

Wystąpił ten błąd, ponieważ przez pomyłkę wielokrotnie wywoływałem metodę usuwania określonego wiersza z widoku recyclingu. Miałem taką metodę jak:

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

Przypadkowo wywoływałem tę metodę trzy razy zamiast raz, więc za drugim razem locbyło -1, a przy próbie jej usunięcia wystąpił błąd. Dwie poprawki miały zapewnić, że metoda została wywołana tylko raz, a także dodać kontrolę poprawności w następujący sposób:

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

0

Mam ten sam problem i czytałem, że stało się to tylko w telefonach Samsung ... Ale rzeczywistość pokazała, że ​​dzieje się tak w wielu markach.

Po testach zdałem sobie sprawę, że dzieje się tak tylko wtedy, gdy przewijasz szybko RecyclerView, a następnie wracasz za pomocą przycisku Wstecz lub przycisku W górę. Więc umieściłem przycisk „w górę” i ponownie włączyłem poniższy fragment kodu:

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

Dzięki temu rozwiązaniu wystarczy załadować nową Arraylist do adaptera i nowy adapter do recyclinglerView, a następnie zakończyć działanie.

Mam nadzieję, że to komuś pomoże


0

Wystąpił ten błąd, ponieważ przez pomyłkę wywoływałem „powiadomItemInserted”.


0

W moim przypadku na liście było ponad 5000 pozycji. Mój problem polegał na tym, że podczas przewijania widoku recyklera czasami wywoływana jest metoda „onBindViewHolder”, a metoda „myCustomAddItems” zmienia listę.

Moim rozwiązaniem było dodanie „synchronized (syncObject) {}” do wszystkich metod zmieniających listę danych. W ten sposób tylko jedna metoda może odczytać tę listę.


0

W moim przypadku dane adaptera uległy zmianie. I niesłusznie użyłem powiadomieniaItemInserted () dla tych zmian. Gdy korzystam z powiadomieniaItemChanged, błąd zniknął.


0

Zetknąłem się z tym samym problemem, gdy usunąłem i zaktualizowałem elementy na liście ... Po wielu dniach badań wydaje mi się, że w końcu znalazłem rozwiązanie.

To, co musisz zrobić, to najpierw zrobić całą notifyItemChangedlistę, a dopiero potem wszystko notifyItemRemoved w kolejności malejącej

Mam nadzieję, że pomoże to osobom, które mają ten sam problem ...


0

Używam kursora, więc nie mogę używać DiffUtils, jak zaproponowano w popularnych odpowiedziach. Aby działało dla mnie, wyłączam animacje, gdy lista nie jest bezczynna. To rozszerzenie rozwiązuje ten problem:

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

Następnie możesz zaktualizować adapter w ten sposób

list.executeSafely {
  adapter.updateICursor(newCursor)
}

0

Jeśli problem występuje po Multi Touch, możesz wyłączyć Multi Touch za pomocą

android:splitMotionEvents="false" 

w pliku układu.


-1

Jeśli Twoje dane bardzo się zmieniają, możesz użyć

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

lub niektóre pojedyncze elementy w zmianach zestawu danych, możesz użyć

 mAdapter.notifyItemChanged(pos);

Aby uzyskać szczegółowe informacje na temat użycia metod, możesz odwołać się do dokumentu , w pewien sposób staraj się nie używać bezpośrednio mAdapter.notifyDataSetChanged().


2
Użycie notifyItemRangeChangedpowoduje również tę samą awarię.
lionelmessi

To pasuje do jakiejś sytuacji. Być może zaktualizowałeś swój zestaw danych zarówno w wątku w tle, jak i wątku interfejsu użytkownika, spowoduje to również niespójność. Jeśli zaktualizujesz zestaw danych tylko w wątku interfejsu użytkownika, zadziała.
Arron Cao,
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.