45

Recently I upgrade my recyclerview-v7:23 to recyclerview-v7:24.2.0. My applicaton has an endless scroll list. The error message points to the line notifyItemInserted when I add loading view into RecyclerView (null object it means loading, id 0 is empty, -1 is end of the page) and it works fine before (recyclerview-v7:23) but suddenly I got error like this and somehow my loading show up twice then when it removed one, there is one loading that still visible in the top.

    W/RecyclerView: Cannot call this method in a scroll callback. Scroll callbacks might be run during a measure & layout pass where you cannot change the RecyclerView data. Any method call that might change the structure of the RecyclerView or the adapter contents should be postponed to the next frame.java.lang.IllegalStateException:  
 at android.support.v7.widget.RecyclerView.assertNotInLayoutOrScroll(RecyclerView.java:2403)
     at android.support.v7.widget.RecyclerView$RecyclerViewDataObserver.onItemRangeInserted(RecyclerView.java:4631)
     at android.support.v7.widget.RecyclerView$AdapterDataObservable.notifyItemRangeInserted(RecyclerView.java:10469)
     at android.support.v7.widget.RecyclerView$Adapter.notifyItemInserted(RecyclerView.java:6211)
     at com.sketchproject.infogue.fragments.MessageFragment.loadMessages(MessageFragment.java:109)
     at com.sketchproject.infogue.fragments.MessageFragment.access$100(MessageFragment.java:42)
     at com.sketchproject.infogue.fragments.MessageFragment$1.onLoadMore(MessageFragment.java:87)
     at com.sketchproject.infogue.modules.EndlessRecyclerViewScrollListener.onScrolled(EndlessRecyclerViewScrollListener.java:74)

When I remove the part of error (add loading) it works fine again, I don't know why, is the newer version of recyclerview prevent adding data into adapter too fast or there is callback function that triggered when "measure & layout" of recyclerview finished, this is my code

private void loadArticles(final int page) {
        if (!isEndOfPage && apiArticleUrl != null) {
            if (swipeRefreshLayout == null || !swipeRefreshLayout.isRefreshing()) {
                allArticles.add(null);
                articleAdapter.notifyItemInserted(allArticles.size() - 1); // error here
            }

            JsonObjectRequest articleRequest = new JsonObjectRequest(Request.Method.GET, apiArticleUrl, null,
                    new Response.Listener<JSONObject>() {
                        @Override
                        public void onResponse(JSONObject response) {

                            try {
                                String status = response.getString("status");
                                JSONObject articles = response.getJSONObject("articles");

                                String nextUrl = articles.getString("next_page_url");
                                int currentPage = articles.getInt("current_page");
                                int lastPage = articles.getInt("last_page");
                                JSONArray data = articles.optJSONArray("data");

                                apiArticleUrl = nextUrl;

                                if (status.equals(APIBuilder.REQUEST_SUCCESS)) {
                                    if (swipeRefreshLayout == null || !swipeRefreshLayout.isRefreshing()) {
                                        allArticles.remove(allArticles.size() - 1);
                                        articleAdapter.notifyItemRemoved(allArticles.size());
                                    } else {
                                        swipeRefreshLayout.setRefreshing(false);
                                        int total = allArticles.size();
                                        for (int i = 0; i < total; i++) {
                                            allArticles.remove(0);
                                        }
                                        articleAdapter.notifyItemRangeRemoved(0, total);
                                    }

                                    List<Article> moreArticles = new ArrayList<>();

                                    if (data != null) {
                                        for (int i = 0; i < data.length(); i++) {
                                            JSONObject articleData = data.getJSONObject(i);
                                            Article article = new Article();
                                            article.setId(articleData.getInt(Article.ID));
                                            article.setSlug(articleData.getString(Article.SLUG));
                                            article.setTitle(articleData.getString(Article.TITLE));
                                            article.setFeatured(articleData.getString(Article.FEATURED_REF));
                                            article.setCategoryId(articleData.getInt(Article.CATEGORY_ID));
                                            article.setCategory(articleData.getString(Article.CATEGORY));
                                            article.setSubcategoryId(articleData.getInt(Article.SUBCATEGORY_ID));
                                            article.setSubcategory(articleData.getString(Article.SUBCATEGORY));
                                            article.setContent(articleData.getString(Article.CONTENT));
                                            article.setContentUpdate(articleData.getString(Article.CONTENT_UPDATE));
                                            article.setPublishedAt(articleData.getString(Article.PUBLISHED_AT));
                                            article.setView(articleData.getInt(Article.VIEW));
                                            article.setRating(articleData.getInt(Article.RATING_TOTAL));
                                            article.setStatus(articleData.getString(Article.STATUS));
                                            moreArticles.add(article);
                                        }
                                    }

                                    int curSize = articleAdapter.getItemCount();
                                    allArticles.addAll(moreArticles);

                                    if (allArticles.size() <= 0) {
                                        Log.i("INFOGUE/Article", "Empty on page " + page);
                                        isEndOfPage = true;
                                        Article emptyArticle = new Article(0, null, "Empty page");
                                        allArticles.add(emptyArticle);
                                    } else if (currentPage >= lastPage) {
                                        Log.i("INFOGUE/Article", "End on page " + page);
                                        isEndOfPage = true;
                                        Article endArticle = new Article(-1, null, "End of page");
                                        allArticles.add(endArticle);
                                    }

                                    articleAdapter.notifyItemRangeInserted(curSize, allArticles.size() - 1);
                                } else {
                                    Log.i("INFOGUE/Article", "Error on page " + page);
                                    Helper.toastColor(getContext(), R.string.error_unknown, R.color.color_warning_transparent);

                                    isEndOfPage = true;
                                    Article failureArticle = new Article();
                                    failureArticle.setId(-2);
                                    failureArticle.setTitle(getString(R.string.error_unknown));
                                    allArticles.add(failureArticle);
                                }
                            } catch (JSONException e) {
                                e.printStackTrace();
                            }
                        }
                    },
                    new Response.ErrorListener() {
                        @Override
                        public void onErrorResponse(VolleyError error) {
                            error.printStackTrace();

                            if (swipeRefreshLayout != null && swipeRefreshLayout.isRefreshing()) {
                                swipeRefreshLayout.setRefreshing(false);
                            }

                            // remove last loading
                            allArticles.remove(allArticles.size() - 1);
                            articleAdapter.notifyItemRemoved(allArticles.size());

                            String errorMessage = getString(R.string.error_unknown);
                            NetworkResponse networkResponse = error.networkResponse;
                            if (networkResponse == null) {
                                if (error.getClass().equals(TimeoutError.class)) {
                                    errorMessage = getString(R.string.error_timeout);
                                } else if (error.getClass().equals(NoConnectionError.class)) {
                                    errorMessage = getString(R.string.error_no_connection);
                                }
                            } else {
                                if (networkResponse.statusCode == 404) {
                                    errorMessage = getString(R.string.error_not_found);
                                } else if (networkResponse.statusCode == 500) {
                                    errorMessage = getString(R.string.error_server);
                                } else if (networkResponse.statusCode == 503) {
                                    errorMessage = getString(R.string.error_maintenance);
                                }
                            }
                            Helper.toastColor(getContext(), errorMessage, R.color.color_danger_transparent);

                            // add error view holder
                            isEndOfPage = true;
                            Article errorArticle = new Article();
                            errorArticle.setId(-2);
                            errorArticle.setTitle(errorMessage);
                            allArticles.add(errorArticle);
                        }
                    }
            );

            articleRequest.setTag("articles");
            articleRequest.setRetryPolicy(new DefaultRetryPolicy(
                    APIBuilder.TIMEOUT_SHORT,
                    DefaultRetryPolicy.DEFAULT_MAX_RETRIES,
                    DefaultRetryPolicy.DEFAULT_BACKOFF_MULT));
            VolleySingleton.getInstance(getContext()).addToRequestQueue(articleRequest);
        }
    }

that code was called from onCreate method

    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
            View view = inflater.inflate(R.layout.fragment_article_list, container, false);

            // Set the adapter
            if (view instanceof RecyclerView) {
                Context context = view.getContext();
                recyclerView = (RecyclerView) view;

                // determine column of list
                LinearLayoutManager linearLayoutManager;
                if (mColumnCount <= 1) {
                    linearLayoutManager = new LinearLayoutManager(context);
                } else {
                    linearLayoutManager = new GridLayoutManager(context, mColumnCount);
            }

        // if article list authored by logged user then prefer editable view holder
        if (mMyArticle) {
            articleAdapter = new ArticleRecyclerViewAdapter(allArticles, mArticleListListener, mArticleEditableListener);
        } else {
            articleAdapter = new ArticleRecyclerViewAdapter(allArticles, mArticleListListener, hasHeader);
        }

        // set the adapter and attach custom scroll listener that triggered onLoadMore() and onReachTop()
        recyclerView.setAdapter(articleAdapter);
        recyclerView.setLayoutManager(linearLayoutManager);
        recyclerView.addOnScrollListener(new EndlessRecyclerViewScrollListener(linearLayoutManager) {
            @Override
            public void onLoadMore(final int page, int totalItemsCount) {
                if (!isFirstCall) {
                    loadArticles(page);
                }
            }

            @Override
            public void onReachTop(boolean isFirst) {
                // activate swipe function when list reach top only, find out where do fragment attached
                if (getActivity() instanceof ArticleActivity) {
                    ((ArticleActivity) getActivity()).setSwipeEnable(isFirst);
                } else if (getActivity() instanceof ApplicationActivity) {
                    ((ApplicationActivity) getActivity()).setSwipeEnable(isFirst);
                }
            }
        });

        if (isFirstCall) {
            isFirstCall = false;
            loadArticles(0);
        }
    }
    return view;
} 

My questions are:

  1. Does the issue come from new version of RecyclerView?
  2. Is it wrong to implement notifyItemInserted inside scroll listener? It worked before.
  3. How can I solve this problem?

Updated

when I logged the code inside first call and scroll,
09-12 03:49:10.078 7046-7046/com.sketchproject.infogue I/Infogue/Contributor: Followers URL http://192.168.43.141:8000/api/contributor/support/followers?contributor_id=1
09-12 03:49:26.421 7046-7046/com.sketchproject.infogue I/Infogue/Contributor: Followers first call
09-12 03:49:26.421 7046-7046/com.sketchproject.infogue I/Infogue/Contributor: Followers URL http://192.168.43.141:8000/api/contributor/support/followers?contributor_id=1
09-12 03:49:26.617 7046-7046/com.sketchproject.infogue I/Infogue/Contributor: Followers second call (scroll)
09-12 03:49:26.618 7046-7046/com.sketchproject.infogue I/Infogue/Contributor: Followers URL http://192.168.43.141:8000/api/contributor/support/followers?contributor_id=1
09-12 03:49:27.365 7046-7046/com.sketchproject.infogue I/Infogue/Contributor: Followers second call (scroll)

They are called twice when first load, after first call and add loading view somehow the scroll is triggered and call again.

Johnny Five
  • 987
  • 1
  • 14
  • 29
Angga Ari Wijaya
  • 1,759
  • 1
  • 15
  • 31

4 Answers4

102

You could also post using the view.

   recyclerView.post(new Runnable() {
        public void run() {
            articleAdapter.notifyItemInserted(allArticles.size() - 1);
        }
    });
scottyab
  • 23,621
  • 16
  • 94
  • 105
  • 3
    Beware of modifying a recyclerView adapter outside of the UI thread... That might lead to an annoying java.lang.IndexOutOfBoundsException: Inconsistency detected. Invalid item position" if somewhere else the adapter is modified. – FlorianT May 19 '18 at 19:33
  • Just a quick message to +1 @FlorianT warning, this exception will hapen. – Mostrapotski Aug 19 '19 at 09:01
35
  1. The issue is not with new version of Recyclerview.

2 & 3. You cannot change item while it is setting (with calling onBindViewHolder). In that case you have to call notifyItemInserted at the end of current loop by calling Handler.post()

Handler handler = new Handler();

    final Runnable r = new Runnable() {
        public void run() {
            articleAdapter.notifyItemInserted(allArticles.size() - 1);
        }
    };

    handler.post(r);

I hope, it will solve your problem.

Sachin Saxena
  • 604
  • 5
  • 10
  • 1
    `recyclerView.addOnScrollListener(new EndlessRecyclerViewScrollListener(linearLayoutManager) { @Override public void onLoadMore(final int page, int totalItemsCount) { if (!isFirstCall) { loadArticles(page); } } });` but it's called twice when the activity show up, there is a way to prevent that by move `isFirstCall = false;` after first request finished (success/error) then I think the newest RecyclerView has different onScroll behavior – Angga Ari Wijaya Sep 12 '16 at 08:17
  • 1
    Your should call loadArticles(page) method when there is 4 items left below the screen. Follow this article https://guides.codepath.com/android/Endless-Scrolling-with-AdapterViews-and-RecyclerView – Sachin Saxena Sep 12 '16 at 09:17
  • 1
    yes, I follow that tutorial and it works fine before, But now the problem has been solved.. – Angga Ari Wijaya Sep 12 '16 at 09:55
  • 1
    Ok that's great!! – Sachin Saxena Sep 12 '16 at 10:02
  • 1
    What was the exact problem? – Sachin Saxena Sep 12 '16 at 10:08
  • Like I said before, the Recyclerview triggered onScroll event when adapter was set,, so they make 2-3 request to the network at once,, and add 2-3 loading view into list as well – Angga Ari Wijaya Sep 12 '16 at 10:17
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/123202/discussion-between-sachin-saxena-and-angga-ari-wijaya). – Sachin Saxena Sep 13 '16 at 04:47
  • To whom it may concern, in some instances I was "randomly" getting an `IndexOutOfBoundsException: Inconsistency detected. Invalid view holder adapter positionViewHolder` from the `RecyclerView` apparently because I was removing data (in my case) outside the handler. I had to move that call into the handler too in order to stop getting it. – Aspiring Dev Mar 01 '17 at 23:17
3

You could also use Google Play Service's Tasks API

Tasks.call(new Callable<Void>() {
    @Override
    public Void call() throws Exception {
        allArticles.add(null);
        articleAdapter.notifyItemInserted(allArticles.size() - 1);
        return null;
    }
});
wonsuc
  • 3,498
  • 1
  • 27
  • 30
  • 2
    Why do you need to use Google's Tasks API, if Android SDK and java provide you with necessary tools, which seems more natural. – Johnny Five May 07 '18 at 11:39
0

In some cases onScrollStateChanged may be enough

override fun onScrollStateChanged(recyclerView: RecyclerView?, newState: Int) {
    super.onScrollStateChanged(recyclerView, newState)
    adapter.notifyDataSetChanged()
}
Tigran Babajanyan
  • 1,967
  • 1
  • 22
  • 41