异常 java.lang.IndexOutOfBoundsException: 检测到不一致。无效的视图持有者适配器位置 ViewHolder

7

我正在开发安卓应用程序,发布到应用商店后,我多次遇到了这个问题。

Exception java.lang.IndexOutOfBoundsException: Inconsistency detected.Invalid view holder adapter positionViewHolder{2f9d83c position=11 id=-1, oldPos=-1, pLpos:-1 no parent}
android.support.v7.widget.RecyclerView$Recycler.validateViewHolderForOffsetPosition (RecyclerView.java:5297)
android.support.v7.widget.RecyclerView$Recycler.tryGetViewHolderForPositionByDeadline (RecyclerView.java:5479)
android.support.v7.widget.GapWorker.prefetchPositionWithDeadline (GapWorker.java:282)
android.support.v7.widget.GapWorker.flushTaskWithDeadline (GapWorker.java:336)
android.support.v7.widget.GapWorker.flushTasksWithDeadline (GapWorker.java:349)
android.support.v7.widget.GapWorker.prefetch (GapWorker.java:356)
android.support.v7.widget.GapWorker.run (GapWorker.java:387)
android.os.Handler.handleCallback (Handler.java:815)
android.os.Handler.dispatchMessage (Handler.java:104)
android.os.Looper.loop (Looper.java:207)
android.app.ActivityThread.main (ActivityThread.java:5737)
java.lang.reflect.Method.invoke (Method.java)
com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run (ZygoteInit.java:789)
com.android.internal.os.ZygoteInit.main (ZygoteInit.java:679)

我花了很长时间搜索这个问题,并尝试了许多解决方案,但异常仍然出现。

我尝试过的其中一个解决方案是创建自定义LinearLayoutManager:

public class WrapContentLinearLayoutManager  extends LinearLayoutManager {

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

    //... constructor
    @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        try {
            super.onLayoutChildren(recycler, state);
            setAutoMeasureEnabled(false);
        } catch (IndexOutOfBoundsException e) {
            Log.e("Error", "IndexOutOfBoundsException in RecyclerView happens");
        }
    }

    public WrapContentLinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
        super(context, orientation, reverseLayout);
        setAutoMeasureEnabled(false);
    }

    public WrapContentLinearLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        setAutoMeasureEnabled(false);
    }

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

在我的应用程序中,需要使用具有“加载更多”和“下拉刷新”功能的RecyclerView。我使用Volley从Web服务获取数据以实现这些功能。
为了在RecyclerView中加载更多项,在Volley响应的回调函数中,我将新项附加到ArrayList中并通知适配器,如下所示:
    final int positionStart = list.size() - 1;
    final int itemCount = response.length();
    for (int i=0;i<response.length();i++)
    {
        try 
        {
            obj = response.getJSONObject(i);
            list.add(new ItemEntry(obj.getInt("Id"),obj.getString("Title").trim()obj.getBoolean("isFavorite"));
        } catch (JSONException e) {
            e.printStackTrace();
        }
        if(index == 0)
        { 
            // for first Initialize for adapter 
            adapter = new MyAdapter(latestEntryList,getActivity());
            adapter.setOnItemClickCallBack(mainFragment);
            recyclerView.setAdapter(adapter);
        }
        else
        {
            // for load more items
            recyclerView.post(new Runnable() 
            {
                @Override
                public void run() 
                {
                   adapter.updateItems(positionStart,itemCount);
                }
            });
        }
    }

适配器.UpdateItems的代码
  public void updateItems(int positionStart,int itemCount) {
        itemsCount = getItemCount();
        this.notifyItemRangeInserted(positionStart,itemCount);
    }

在进行“加载更多”时,我会在OnRefresh中执行以下操作。
   public void onRefresh() {
        listFragment.LatestEntry(0,false);
// 0 the index and False isAppend to list of not
    } 

LatestEntry的代码如下:
   private void LatestEntry(final int index, final boolean isAppend)
        {
        String customerId = "123";
        String uri = String.format(BASE_URL+"/api/Entry/LatestEntry?customerId=%s&index=%s",customerId,index);

            JsonArrayRequest LatestEntryJsonArrayRequest = new JsonArrayRequest(Request.Method.GET, uri,
                null, new Response.Listener<JSONArray>() {
                @Override
                public void onResponse(JSONArray response) 
                   {
                        JSONObject obj = new JSONObject();
                        if(response.length() > 0 && !isAppend) 
                        {
                            int startPosition = 0;
                            int preSize;

                            if(adapter != null)
                                preSize = adapter.entries.size();
                            else
                                preSize = 0;
                            if(preSize > 0 && EntryList.size() > 0) {
                                EntryList.clear();
                                adapter.entries.clear();
                                adapter.notifyItemRangeRemoved(startPosition, preSize);
                            }
                        }

                        final int positionStart = EntryList.size() - 1;
                        final int itemCount = response.length();
                        for (int i=0;i<response.length();i++)
                        {
                            try {
                                obj = response.getJSONObject(i);
                                list.add(new ItemEntry
                               (obj.getInt("Id"),obj.getString("Title").trim()
                                  obj.getBoolean("isFavorite"));

                            } catch (JSONException e) {
                                e.printStackTrace();
                            }
                        }

                        if(index == 0) 
                        { // for first Initialize for adapter 
                            adapter = new MyAdapter(latestEntryList,getActivity());
                            adapter.setOnItemClickCallBack(mainFragment);
                            recyclerView.setAdapter(adapter);
                        }
                        else
                        {
                            // for load more items
                            recyclerView.post(new Runnable() 
                            {
                                @Override
                                public void run() {
                                   adapter.updateItems(positionStart,itemCount);
                                }
                            });
                        }
                    }
                    setListShownNoAnimation(true);
                    if(mainFragment.swipeRefreshLayout.isRefreshing())
                      mainFragment.swipeRefreshLayout.setRefreshing(false);
                }
            }, new Response.ErrorListener() {
                @Override
                public void onErrorResponse(VolleyError error) {
                    Toast.makeText(MyApplication.getAppContext(),error.getMessage(),Toast.LENGTH_LONG);
                }
            }
            );

            LatestEntryJsonArrayRequest.setTag("LatestEntry");
            KidsSingleton.getInstance().addToRequestQueue(LatestEntryJsonArrayRequest);
        }

请注意以下内容:
  1. Recyclerview版本为:recyclerview-v7:25.3.0
  2. 我在ViewPager中有三个RecylerView
  3. 每个ViewPager包含一个Fragment,该Fragment包含ListFragment和RecylerView。
  4. 所有RecylcrViews都使用一个适配器。
有人能帮助解决这个问题吗?

你的recyclerview支持库版本是多少? - Burhanuddin Rashid
我使用的是recyclerview-v7:25.3.0版本。 - Mohammed K. Abuhalib
FYI:onLayoutChildren()在这里不是问题——您的堆栈跟踪未显示其列表中。 - Someone Somewhere
我这里也遇到了完全相同的问题。看起来是“预取”功能的一个错误。这些错误只在Google于25.1.0版本中添加此功能后出现。 - Kimi Chiu
4个回答

1

我有类似的情况 - 在Recycler View中,ViewPager作为项目。在添加/删除项目时,“等待被移除”的项目有一些动画效果。因此,当我调用删除项目时,如果动画仍在执行,就会出现以下情况:

IllegalStateException: Cannot call this method while RecyclerView is computing a layout or scrolling

为了避免这个异常,我创建了一个处理程序来延迟删除项目。但是!这是错误的。如果您更改了适配器的数据,则RV应立即被通知这些更改,而无需任何处理程序或多线程。 因此,请考虑将此方式更改为不需要单独线程的内容更新方式。

0

RecyclerView$Recycler.validateViewHolderForOffsetPosition 抱怨没有收到一个有效的 ViewHolder 项目,而它似乎在第11个项目的偏移位置。

我会猜测你可能忘记了在向适配器添加更多项目时先填充 ViewHolder...处理方式似乎不正确;一般来说,只需要更新数据数组,然后使用例如notifyDataChanged()通知适配器;当直接向适配器添加项目时,这些通知会在内部触发。

swipeRefreshLayout.setRefreshing(false) 可能应该在回调方法内执行,这样加载完成后,旋转动画就会消失。

也许可以查看这个example 来了解框架触发事件的顺序(因此往往问题是,您创建了框架已经具备的功能)...进入可能还可以提供线索,为什么没有为该项填充有效的 ViewHolder


我尝试了您的建议,现在在“加载更多”时,我将数据添加到临时列表中,然后一次性更新adapter列表,然后调用notifyItemRangeInserted,但是在我将应用程序发布到Google Play之后,异常又出现了。 - Mohammed K. Abuhalib
@MohammedK.Abuhalib 只能猜测,因为没有看到崩溃报告;很有可能是 ProGuard 混淆了太多(甚至对该类有 -dontnote-dontwarn 标志,因此不会抱怨未知的类)... 删除(或注释掉)这些配置规则将是我采取的第一步。上传 mapping.txt 也很有用。 - Martin Zeitler
我不使用ProGuard混淆,也不使用“-dontnote”或“-dontwarn”标记,而且proguard-rules.pro文件为空。 - Mohammed K. Abuhalib

0
可能是您设置给适配器的列表已经发生了变化,但是尚未调用notifyDataSetChanged方法。 在我的情况下,我延迟了调用notifyDataSetChanged方法。但是适配器的getItemCount()方法已经改变为另一个值,因此它会崩溃。

-1

在调用notifyItemRangeInserted(positionStart,itemCount);之前,先调用notifyItemRangeChanged(positionStart,itemCount)

这对我很有效。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接