Android RecyclerView:notifyDataSetChanged() IllegalStateException

Posted

技术标签:

【中文标题】Android RecyclerView:notifyDataSetChanged() IllegalStateException【英文标题】:Android RecyclerView : notifyDataSetChanged() IllegalStateException 【发布时间】:2015-01-20 02:55:30 【问题描述】:

我正在尝试使用 notifyDataSetChanged() 更新回收视图的项目。

这是我在 recycleview 适配器中的 onBindViewHolder() 方法。

@Override
public void onBindViewHolder(ViewHolder viewHolder, int position) 

     //checkbox view listener
    viewHolder.getCheckbox().setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() 
        @Override
        public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) 

            //update list items
            notifyDataSetChanged();
        
    );

我想要做的是在我选中一个复选框后更新列表项。我得到一个非法异常:"Cannot call this method while RecyclerView is computing a layout or scrolling"

java.lang.IllegalStateException: Cannot call this method while RecyclerView is computing a layout or scrolling
    at android.support.v7.widget.RecyclerView.assertNotInLayoutOrScroll(RecyclerView.java:1462)
    at android.support.v7.widget.RecyclerView$RecyclerViewDataObserver.onChanged(RecyclerView.java:2982)
    at android.support.v7.widget.RecyclerView$AdapterDataObservable.notifyChanged(RecyclerView.java:7493)
    at android.support.v7.widget.RecyclerView$Adapter.notifyDataSetChanged(RecyclerView.java:4338)
    at com.app.myapp.screens.RecycleAdapter.onRowSelect(RecycleAdapter.java:111)

我也使用了 notifyItemChanged(),同样的例外。有什么秘密方法可以更新以通知适配器发生了变化?

【问题讨论】:

我现在遇到了同样的问题。将 setoncheckchanged 侦听器放在 viewholder 构造函数中会给我同样的错误 【参考方案1】:

您应该将方法“setOnCheckedChangeListener()”移动到 ViewHolder,它是适配器的内部类。

onBindViewHolder() 不是初始化ViewHolder 的方法。 此方法是刷新每个回收站项目的步骤。 当你调用notifyDataSetChanged()时,onBindViewHolder()将被调用为每个项目的次数。

所以如果你将notifyDataSetChanged()放入onCheckChanged()并初始化onBindViewHolder()中的checkBox,你会因为循环方法调用而得到IllegalStateException。

点击复选框 -> onCheckedChanged() -> notifyDataSetChanged() -> onBindViewHolder() -> 设置复选框 -> onChecked...

简单地说,您可以通过将一个标志放入适配器来解决此问题。

试试这个,

private boolean onBind;

public ViewHolder(View itemView) 
    super(itemView);
    mCheckBox = (CheckBox) itemView.findViewById(R.id.checkboxId);
    mCheckBox.setOnCheckChangeListener(this);


@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) 
    if(!onBind) 
        // your process when checkBox changed
        // ...

        notifyDataSetChanged();
    


...

@Override
public void onBindViewHolder(YourAdapter.ViewHolder viewHolder, int position) 
    // process other views 
    // ...

    onBind = true;
    viewHolder.mCheckBox.setChecked(trueOrFalse);
    onBind = false;

【讨论】:

我明白了,有道理。希望平台能够预测这种简单的行为并给出解决方案,而不必依赖标志.. 只要在onBindViewHolder() 进行中时不通知AdapterViewObserver,在哪里设置监听器都没有关系。 我更喜欢这个解决方案***.com/a/32373999/1771194,并在评论中有一些改进。它还允许我在 RecyclerView 中创建“RadioGroup”。 如何在我的列表中获取项目位置?? 这在视图中对我不起作用。仍然得到崩溃错误。我需要更改数组列表中的变量。很奇怪。不太确定我可以在哪里附加监听器。【参考方案2】:

您可以在进行更改之前重置之前的侦听器,并且不会出现此异常。

private CompoundButton.OnCheckedChangeListener checkedListener = new CompoundButton.OnCheckedChangeListener()                       
                        @Override
                        public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) 
                            //Do your stuff
                    );;

    @Override
    public void onBindViewHolder(final ViewHolder holder, final int position) 
        holder.checkbox.setOnCheckedChangeListener(null);
        holder.checkbox.setChecked(condition);
        holder.checkbox.setOnCheckedChangeListener(checkedListener);
    

【讨论】:

很好的答案,但最好不要在每个 onBindViewHolder 调用上创建侦听器。将其作为一个字段。 使用字段当然更好,我只是举一个有效的例子。但是感谢您的警告,我会更新答案。 我实际上每次都需要绑定一个新的监听器,因为监听器每次都需要一个更新的位置变量。所以这是一个很好的答案,所以我不必使用处理程序。 这绝对是最好的方法,因为它从不建议保持全局状态,这是公认的答案 (***.com/a/31069171/882251) 所推荐的。 最简单的一个!!谢谢!!【参考方案3】:

使用Handler 添加项目并从此Handler 调用notify...() 为我解决了这个问题。

【讨论】:

这是正确的答案,您不能在设置时更改项目(通过调用 onBindViewHolder)。在这种情况下,您必须通过调用 Handler.post() 在当前循环结束时调用 notifyDataSetChanged @user1232726 如果在主线程上创建 Handler,则不必指定 Looper(默认为调用线程的 Looper)。所以,是的,这是我的建议。否则你也可以手动指定 Looper。 不幸的是,当我向下滚动并再次向上滚动时,我的复选框没有保持选中状态。嘘。大声笑 @user1232726 搜索答案或提出描述您的问题的新问题。 我强烈不鼓励这个答案,因为这是解决问题的一种老套方法。你做的越多,你的代码就越复杂,难以理解。参考Moonsoo's answer了解问题,参考JoniDS's解答解决问题。【参考方案4】:

我不太清楚,但我也有同样的问题。我通过在checkbox 上使用onClickListner 解决了这个问题

viewHolder.mCheckBox.setOnClickListener(new OnClickListener() 
        @Override
        public void onClick(View v) 
            // TODO Auto-generated method stub
            if (model.isCheckboxBoolean()) 
                model.setCheckboxBoolean(false);
                viewHolder.mCheckBox.setChecked(false);
             else 
                model.setCheckboxBoolean(true);
                viewHolder.mCheckBox.setChecked(true);
            
            notifyDataSetChanged();
        
    );

试试这个,这可能会有所帮助!

【讨论】:

干得好)但仅在点击时(如果我慢速移动小部件(SwitchCompat),此操作将被错过。这是唯一的问题【参考方案5】:
protected void postAndNotifyAdapter(final Handler handler, final RecyclerView recyclerView, final RecyclerView.Adapter adapter) 
        handler.post(new Runnable() 
            @Override
            public void run() 
                if (!recyclerView.isComputingLayout()) 
                    adapter.notifyDataSetChanged();
                 else 
                    postAndNotifyAdapter(handler, recyclerView, adapter);
                
            
        );
    

【讨论】:

我想你可以很容易地通知适配器两次。 这是唯一真正最终执行计算的答案,而不是在尝试时它还没有准备好时忽略它。但是,对于这么大的调用堆栈,这不会导致内存不足错误吗? post 调用下一个递归调用需要多长时间?【参考方案6】:

找到一个简单的解决方案 -

public class MyAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>

    private RecyclerView mRecyclerView; 

    @Override
    public void onAttachedToRecyclerView(RecyclerView recyclerView) 
        super.onAttachedToRecyclerView(recyclerView);
        mRecyclerView = recyclerView;
    

    private CompoundButton.OnCheckedChangeListener checkedChangeListener 
    = (compoundButton, b) -> 
        final int position = (int) compoundButton.getTag();
        // This class is used to make changes to child view
        final Event event = mDataset.get(position);
        // Update state of checkbox or some other computation which you require
        event.state = b;
        // we create a runnable and then notify item changed at position, this fix crash
        mRecyclerView.post(new Runnable() 
            @Override public void run() 
                notifyItemChanged(position));
            
        );
    

这里我们创建一个runnable来 notifyItemChanged 当recyclerview准备好处理它的位置。

【讨论】:

这比标记为正确的解决方案更好。【参考方案7】:

出现消息错误时:

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

简单,只要做导致异常的原因:

RecyclerView.post(new Runnable() 
    @Override
    public void run() 
        /** 
        ** Put Your Code here, exemple:
        **/
        notifyItemChanged(position);
    
);

【讨论】:

这对我有用。想知道这个解决方案有什么问题吗?【参考方案8】:

起初我认为Moonsoo's answer(已接受的答案)对我不起作用,因为我无法在 ViewHolder 构造函数中初始化我的setOnCheckedChangeListener(),因为我需要每次都绑定它,以便它获得更新的位置变量。但我花了很长时间才明白他在说什么。

这是他所说的“循环方法调用”的一个例子:

public void onBindViewHolder(final ViewHolder holder, final int position) 
    SwitchCompat mySwitch = (SwitchCompat) view.findViewById(R.id.switch);
    mySwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() 
                @Override
                public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) 
                       if (isChecked) 
                           data.delete(position);
                           notifyItemRemoved(position);
                           //This will call onBindViewHolder, but we can't do that when we are already in onBindViewHolder!
                           notifyItemRangeChanged(position, data.size());
                       
                   
            );
    //Set the switch to how it previously was.
    mySwitch.setChecked(savedSwitchState); //If the saved state was "true", then this will trigger the infinite loop.

唯一的问题是,当我们需要将开关初始化为打开或关闭时(例如,从过去保存的状态),它正在调用可能调用nofityItemRangeChanged 的侦听器,它再次调用onBindViewHolder .当您已经在 onBindViewHolder] 中时,您不能调用 onBindViewHolder,因为 如果您已经在通知项目范围已更改,则不能 notifyItemRangeChanged 但我只需要更新 UI 以打开或关闭它,而不希望实际触发任何东西。

这是我从JoniDS's answer 学到的解决方案,可以防止无限循环。 只要我们在设置Checked之前将监听器设置为“null”,那么它会在不触发监听器的情况下更新UI,避免死循环。然后我们可以在之后设置监听器。

JoniDS 的代码:

holder.checkbox.setOnCheckedChangeListener(null);
holder.checkbox.setChecked(condition);
holder.checkbox.setOnCheckedChangeListener(checkedListener);

我的示例的完整解决方案:

public void onBindViewHolder(final ViewHolder holder, final int position) 
    SwitchCompat mySwitch = (SwitchCompat) view.findViewById(R.id.switch);

    //Set it to null to erase an existing listener from a recycled view.
    mySwitch.setOnCheckedChangeListener(null);

    //Set the switch to how it previously was without triggering the listener.
    mySwitch.setChecked(savedSwitchState); //If the saved state was "true", then this will trigger the infinite loop.

    //Set the listener now.
    mySwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() 
        @Override
        public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) 
            if (isChecked) 
                data.delete(position);
                notifyItemRemoved(position);
                //This will call onBindViewHolder, but we can't do that when we are already in onBindViewHolder!
                notifyItemRangeChanged(position, data.size());
            
        
    );

【讨论】:

您应该避免在 onBindViewHolder 中一次又一次地初始化 OnCheckedChangeListener(这样需要的 GC 较少)。这应该在 onCreateViewHolder 中调用,然后通过调用 holder.getAdapterPosition() 获得位置。【参考方案9】:

当您调用 notifyDataSetChanged(); 时,您的 CheckBox 项目正在更改可绘制对象,因此会发生此异常。 尝试在您的视图中致电notifyDataSetChanged();。例如:

buttonView.post(new Runnable() 
                    @Override
                    public void run() 
                        notifyDataSetChanged();
                    
                );

【讨论】:

【参考方案10】:

为什么不检查RecyclerView.isComputingLayout() 状态如下?

public class MyAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>

    private RecyclerView mRecyclerView; 

    @Override
    public void onAttachedToRecyclerView(RecyclerView recyclerView) 
        super.onAttachedToRecyclerView(recyclerView);
        mRecyclerView = recyclerView;
    

    @Override
    public void onBindViewHolder(ViewHolder viewHolder, int position) 

        viewHolder.getCheckbox().setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() 
            @Override
            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) 
                if (mRecyclerView != null && !mRecyclerView.isComputingLayout()) 
                    notifyDataSetChanged();
                
            
        );
    

【讨论】:

【参考方案11】:

当布局管理器绑定项目时,很可能您正在设置复选框的选中状态,这会触发回调。

当然这是一个猜测,因为您没有发布完整的堆栈跟踪。

当 RV 重新计算布局时,您无法更改适配器内容。如果项目的选中状态等于回调中发送的值(如果调用 checkbox.setChecked 触发回调,则会出现这种情况),您可以通过不调用 notifyDataSetChanged 来避免它。

【讨论】:

谢谢@yigit!我的问题与复选框无关,而是更复杂的情况,我必须通知适配器中的不同项目,但我遇到了类似的崩溃。我更新了我的通知逻辑,只在数据实际发生变化并且解决了我的崩溃时才更新。所以我对 RecyclerViews 的新规则:当什么都没有改变时,不要通知有什么改变了。非常感谢您的回答!【参考方案12】:

在复选框上使用onClickListner而不是OnCheckedChangeListener,它会解决问题

viewHolder.myCheckBox.setOnClickListener(new OnClickListener() 
        @Override
        public void onClick(View v) 
            if (viewHolder.myCheckBox.isChecked()) 
                // Do something when checkbox is checked
             else 
                // Do something when checkbox is unchecked                
            
            notifyDataSetChanged();
        
    );

【讨论】:

【参考方案13】:

notifyDataSetChanged() 之前,只需使用以下方法检查:recyclerView.IsComputingLayout()

【讨论】:

【参考方案14】:

简单使用帖子:

new Handler().post(new Runnable() 
        @Override
        public void run() 
                mAdapter.notifyItemChanged(mAdapter.getItemCount() - 1);
            
        
    );

【讨论】:

【参考方案15】:

我被这个问题困扰了一个小时,这就是你可以解决的方法。 但在开始之前,此解决方案有一些条件。

模型类

public class SelectUserModel 

    private String userName;
    private String UserId;
    private Boolean isSelected;


    public String getUserName() 
        return userName;
    

    public void setUserName(String userName) 
        this.userName = userName;
    

    public String getUserId() 
        return UserId;
    

    public void setUserId(String userId) 
        UserId = userId;
    

    public Boolean getSelected() 
        return isSelected;
    

    public void setSelected(Boolean selected) 
        isSelected = selected;
    

适配器类中的复选框

CheckBox cb;

适配器类构造器和模型列表

private List<SelectUserModel> userList;

public StudentListAdapter(List<SelectUserModel> userList) 
        this.userList = userList;

        for (int i = 0; i < this.userList.size(); i++) 
            this.userList.get(i).setSelected(false);
        
    

ONBINDVIEW [请使用 onclick 代替 onCheckChange]

public void onBindViewHolder(@NonNull final StudentListAdapter.ViewHolder holder, int position) 
    holder.cb.setChecked(user.getSelected());
    holder.cb.setOnClickListener(new View.OnClickListener() 
        @Override
        public void onClick(View view) 

            int pos = (int) view.getTag();
            Log.d(TAG, "onClick: " + pos);
            for (int i = 0; i < userList.size(); i++) 
                if (i == pos) 
                    userList.get(i).setSelected(true);
// an interface to listen to callbacks
                    clickListener.onStudentItemClicked(userList.get(i));
                 else 
                    userList.get(i).setSelected(false);
                
            
            notifyDataSetChanged();
        
    );

【讨论】:

哇,这是最好的答案。【参考方案16】:

我遇到了这个确切的问题!在 Moonsoo 的回答并没有真正让我的船浮起来之后,我搞砸了一点,找到了一个适合我的解决方案。

首先,这是我的一些代码:

    @Override
    public void onBindViewHolder(ViewHolder holder, final int position) 

    final Event event = mDataset.get(position);

    //
    //  .......
    //

    holder.mSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() 
        @Override
        public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) 
            event.setActive(isChecked);
            try 
                notifyItemChanged(position);
             catch (Exception e) 
                Log.e("onCheckChanged", e.getMessage());
            
        
    );

您会注意到我是专门通知适配器我正在更改的位置,而不是像您正在做的整个数据集。话虽如此,虽然我不能保证这对你有用,但我通过将我的 notifyItemChanged() 调用包装在 try/catch 块中解决了这个问题。这只是捕获了异常,但仍然允许我的适配器注册状态更改并更新显示!

希望这对某人有所帮助!

编辑:我承认,这可能不是处理问题的正确/成熟方式,但由于不处理异常似乎不会造成任何问题,我想我会分享以防万一对其他人来说已经足够了。

【讨论】:

【参考方案17】:

发生这种情况是因为您可能在配置该行的值之前设置了“侦听器”,这会使侦听器在您为复选框“配置值”时被触发。

你需要做的是:

@Override
public void onBindViewHolder(YourAdapter.ViewHolder viewHolder, int position) 
   viewHolder.mCheckBox.setOnCheckedChangeListener(null);
   viewHolder.mCheckBox.setChecked(trueOrFalse);
   viewHolder.setOnCheckedChangeListener(yourCheckedChangeListener);

【讨论】:

【参考方案18】:
        @Override
        public void onBindViewHolder(final MyViewHolder holder, final int position) 
            holder.textStudentName.setText(getStudentList.get(position).getName());
            holder.rbSelect.setChecked(getStudentList.get(position).isSelected());
            holder.rbSelect.setTag(position); // This line is important.
            holder.rbSelect.setOnClickListener(onStateChangedListener(holder.rbSelect, position));

        

        @Override
        public int getItemCount() 
            return getStudentList.size();
        
        private View.OnClickListener onStateChangedListener(final RadioButton checkBox, final int position) 
            return new View.OnClickListener() 
                @Override
                public void onClick(View v) 
                    if (checkBox.isChecked()) 
                        for (int i = 0; i < getStudentList.size(); i++) 

                            getStudentList.get(i).setSelected(false);

                        
                        getStudentList.get(position).setSelected(checkBox.isChecked());

                        notifyDataSetChanged();
                     else 

                    

                
            ;
        

【讨论】:

【参考方案19】:

只需在onCheckedChanged(CompoundButton compoundButton, boolean isChecked)中使用CompoundButtonisPressed()方法例如

public void onCheckedChanged(CompoundButton compoundButton, boolean isChecked)    
                      ... //your functionality    
                            if(compoundButton.isPressed())
                                notifyDataSetChanged();
                            
                          );

【讨论】:

【参考方案20】:

我在使用 Checkbox 和 RadioButton 时遇到了同样的问题。用notifyItemChanged(position) 替换notifyDataSetChanged() 有效。我在数据模型中添加了一个布尔字段isChecked。然后我更新了布尔值并在onCheckedChangedListener 中调用了notifyItemChanged(adapterPosition)。这可能不是最好的方法,但对我有用。布尔值用于检查该项是否被选中。

【讨论】:

【参考方案21】:

这主要是因为 notifydatasetchanged 调用了复选框的 onCheckedchanged 事件,并且在该事件中再次notifydatasetchanged

要解决它,您只需检查复选框是否被编程检查或用户按下它。有 isPressed 方法。

所以将整个listner 代码包装在isPressed 方法中。并且完成了。

 holder.mBinding.cbAnnual.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() 
            @Override
            public void onCheckedChanged(CompoundButton compoundButton, boolean b) 

                if(compoundButton.isPressed()) 


                       //your code
                        notifyDataSetChanged();   

            
        );

【讨论】:

【参考方案22】:
 override fun bindItemViewHolder(viewHolder: RecyclerView.ViewHolder, position: Int) 
    var item: ThingChannels = items[position]
    rowActionBinding.xCbChannel.text = item.channelType?.primaryType
    rowActionBinding.xTvRoomName.text = item.thingDetail?.room?.name
    rowActionBinding.xCbChannel.isChecked = item.isSelected
    rowActionBinding.xCbChannel.tag = position
    rowActionBinding.xCbChannel.setOnClickListener 
        setSelected(it.tag as Int)
        if (onItemClickListener != null) 
            onItemClickListener!!.onItemClick(position, null)
        
    

【讨论】:

【参考方案23】:

过去的答案都没有解决问题!

过去答案的问题

如果用户正在滚动,他们要么通过吞下更改(即不通知适配器更改)来避免问题,这只是意味着如果用户在更改准备好时正在滚动,他们将永远不会看到改变。或者,他们建议使用 recylerView.post() 来推迟问题。

答案

选项 #1

停止滚动,然后通知适配器:

recyclerView.stopScroll()
val copy = workingList.toList()

//prevent IndexOutOfBoundsException: Inconsistency detected... (https://***.com/questions/41054959/java-lang-indexoutofboundsexception-inconsistency-detected-invalid-view-holder)
workingList.clear()
recyclerViewAdapter?.notifyDataSetChanged()
//set display to correct data 
workingList.addAll(copy)
recyclerViewAdapter?.notifyItemRangeInserted(0, workingList.size)

选项 #2

为了更好的用户体验,您可以让他们继续滚动并在他们停止滚动时进行监听以更新 UI,但仅当您不打算编写将接受不同 @ 的函数时才应使用此方法987654323@ 实例,因为如果您将多个侦听器添加到同一个RecyclerView 都试图更新,应用程序将崩溃:

if(recyclerView.scrollState != RecyclerView.SCROLL_STATE_IDLE) //notify RecyclerView
else recyclerView.addOnScrollListener(object: RecyclerView.OnScrollListener() 
        override fun onScrollStateChanged(
            recyclerView: RecyclerView,
            newState: Int
        ) 
            if(newState == RecyclerView.SCROLL_STATE_IDLE) 
                //notify RecyclerView...

                val copy = workingList.toList()
                
                //prevent IndexOutOfBoundsException: Inconsistency detected... (https://***.com/questions/41054959/java-lang-indexoutofboundsexception-inconsistency-detected-invalid-view-holder)
                workingList.clear()
                recyclerViewAdapter?.notifyDataSetChanged()
                //set to display correct data
                workingList.addAll(copy)
                recyclerViewAdapter?.notifyItemRangeInserted(0, workingList.size)
            
        )

注意:即使您正在编写实用程序函数,也可以使用选项 #2,方法是保留以前注册的实例的列表,如果已经注册,则不要添加侦听器,但它不是“干净的编码”依赖库/实用程序类中的状态。

【讨论】:

【参考方案24】:

对我来说,当我通过 Done、Back 或外部输入触摸退出 EditText 时出现问题。这会导致使用输入文本更新模型,然后通过实时数据观察刷新回收器视图。

问题是光标/焦点保留在 EditText 中。

当我通过使用删除焦点时:

editText.clearFocus() 

回收者视图的通知数据更改方法确实停止抛出此错误。

我认为这是此问题的可能原因/解决方案之一。有可能这个异常可以通过其他方式修复,因为它可能是由完全不同的原因引起的。

【讨论】:

以上是关于Android RecyclerView:notifyDataSetChanged() IllegalStateException的主要内容,如果未能解决你的问题,请参考以下文章

Android RecyclerView嵌套RecyclerView

Android之RecyclerView介绍

Android:RecyclerView里面的RecyclerView,滚动到底部

Android教程2020 - RecyclerView获取滑动距离

android recyclerview怎么设置每次只能滑动一页

Android的import android.support.v7.widget.RecyclerView的RecyclerView标红问题