Android RecyclerView: notifyDataSetChanged () IllegalStateException

130

Я пытаюсь обновить элементы recycleview с помощью notifyDataSetChanged ().

Это мой метод onBindViewHolder () в адаптере recycleview.

@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 дает мне ту же ошибку
filthy_wizard

Ответы:

145

Вам следует переместить метод setOnCheckedChangeListener () в ViewHolder, который является внутренним классом вашего адаптера.

onBindViewHolder()это не метод инициализации ViewHolder. Этот метод является этапом обновления каждого элемента ресайклера. Когда вы звоните notifyDataSetChanged(), onBindViewHolder()будет называться количество раз каждого элемента.

Итак, если вы notifyDataSetChanged()поместите onCheckChanged()и инициализируете checkBox onBindViewHolder(), вы получите исключение IllegalStateException из-за циклического вызова метода.

установите флажок -> onCheckedChanged () -> notifyDataSetChanged () -> onBindViewHolder () -> установите флажок -> onChecked ...

Просто вы можете исправить это, поставив один флаг в Adapter.

попробуй это,

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;
}
Moonsoo Jeong
источник
Понятно, имеет смысл. Хотелось бы, чтобы платформа предсказывала такое простое поведение и давала решение вместо того, чтобы полагаться на флаги ..
Артур
Неважно, где вы установите слушателя, если вы не уведомляете, AdapterViewObserverпока выполняется onBindViewHolder().
Ярослав Мыткалык 06
6
Я предпочитаю это решение stackoverflow.com/a/32373999/1771194 с некоторыми улучшениями в комментариях. Это также позволило мне сделать "RadioGroup" в RecyclerView.
Артем
как мне получить позицию в моем списке ??
filthy_wizard 01
2
это не работает для меня в зрителе. все еще появляется ошибка сбоя. мне нужно изменить вары в Arraylist. очень странно. не слишком уверен, где я могу прикрепить список.
filthy_wizard 03
46

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

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);
    }
JoniDS
источник
2
Хороший ответ, но лучше не создавать слушателя при каждом вызове onBindViewHolder. Сделайте это как поле.
Артем
1
Использование поля, конечно, лучше, я просто привел пример, который работает. Но спасибо за предупреждение, обновлю ответ.
JoniDS
1
На самом деле мне в любом случае нужно каждый раз связывать нового слушателя, потому что слушателю каждый раз требуется обновленная переменная позиции. Так что это отличный ответ, поэтому мне не нужно использовать обработчик.
Рок Ли
Это определенно лучший способ сделать это, поскольку никогда не рекомендуется сохранять глобальное состояние, что рекомендуется в принятом ответе ( stackoverflow.com/a/31069171/882251 ).
Darwind
Самый простой !! Спасибо !!
DalveerSinghDaiya
39

Использование a Handlerдля добавления элементов и звонков notify...()с этого места Handlerрешило проблему для меня.

cybergen
источник
3
Это правильный ответ, вы не можете изменить элемент во время его настройки (с вызовом onBindViewHolder). В этом случае вы должны вызвать notifyDataSetChanged в конце текущего цикла, вызвав Handler.post ()
pjanecze
1
@ user1232726 Если вы создаете обработчик в основном потоке, вам не нужно указывать Looper (по умолчанию это цикл вызывающих потоков). Так что да, это мой совет. В противном случае вы также можете указать лупер вручную.
cybergen
К сожалению, мои флажки не остаются отмеченными, когда я прокручиваю вниз и снова прокручиваю вверх. бу. lol
filthy_wizard 03
@ user1232726 найдите ответ или задайте новый вопрос, описывающий вашу проблему.
cybergen
2
Я бы категорически не одобрял этот ответ, так как это хакерский способ решить проблему. Чем больше вы делаете это, тем сложнее становится ваш код для понимания. Обратитесь к ответу Moonsoo, чтобы понять проблему, и к ответу JoniDS, чтобы решить проблему.
Kalpesh Patel
26

Я плохо знаю, но у меня тоже была такая же проблема. Я решил эту проблему, используя onClickListnerнаcheckbox

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

Попробуйте, это может помочь!

Jigar
источник
1
Хорошая работа) НО только при нажатии (если я медленно перемещаю виджет (SwitchCompat), это действие будет пропущено. Это единственная проблема
Влад
12
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);
                }
            }
        });
    }
брюс
источник
Полагаю, что вы можете легко оповестить адаптер дважды.
Максим Петлюк
8

Когда у вас есть сообщение об ошибке:

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);
    }
});
Антуан Драун
источник
1
Это сработало для меня. Хотите знать, есть ли проблемы с этим решением?
Sayooj Valsan
7

Нашел простое решение -

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 готов обработать ее.

Рохан Кандвал
источник
5

ваш элемент CheckBox изменяется при вызове, notifyDataSetChanged();поэтому возникнет это исключение. Попробуй назвать notifyDataSetChanged();в посте твоего мнения. Например:

buttonView.post(new Runnable() {
                    @Override
                    public void run() {
                        notifyDataSetChanged();
                    }
                });
Мохаммад Реза Норузи
источник
4

Сначала я подумал, что ответ Moonsoo (принятый ответ) не сработает для меня, потому что я не могу инициализировать свойsetOnCheckedChangeListener() в конструкторе ViewHolder, потому что мне нужно каждый раз связывать его, чтобы он получал обновленную переменную позиции. Но мне потребовалось много времени, чтобы понять, о чем он говорит.

Вот пример "кругового вызова метода", о котором он говорит:

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если вы уже в середине уведомления об изменении диапазона элементов. Но мне нужно было только обновить пользовательский интерфейс, чтобы показать его включенным или выключенным, не желая фактически ничего запускать.

Вот решение, которое я узнал из ответа JoniDS, которое предотвратит бесконечный цикл. Пока мы устанавливаем для слушателя значение «null» до того, как мы устанавливаем Checked, он будет обновлять пользовательский интерфейс, не запуская слушателя, избегая бесконечного цикла. Затем мы можем установить слушателя после.

Код 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());
            }
        }
    });
}
Рок Ли
источник
Вам следует избегать инициализации OnCheckedChangeListener снова и снова в onBindViewHolder (в этом случае требуется меньше GC). Предполагается, что это будет вызвано в onCreateViewHolder, и вы получите позицию, вызвав Holder.getAdapterPosition ().
разработчик Android
4

Почему бы не проверить 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();
                }
            }
        });
    }
}
NcJie
источник
2

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

Конечно, это предположение, потому что вы не опубликовали полную трассировку стека.

Вы не можете изменить содержимое адаптера, пока RV выполняет пересчет макета. Вы можете избежать этого, не вызывая notifyDataSetChanged, если отмеченное состояние элемента равно значению, отправленному в обратном вызове (что будет иметь место, если вызов checkbox.setCheckedзапускает обратный вызов).

Yigit
источник
Спасибо @yigit! Моя проблема связана не с флажком, а с более сложной ситуацией, когда мне нужно было уведомить другой элемент в адаптере, но я получал аналогичный сбой. Я обновил свою логику уведомлений, чтобы обновлять только при фактическом изменении данных, и это устранило мой сбой. Итак, мое новое правило с RecyclerViews: не сообщать, что что-то изменилось, когда ничего не изменилось. Большое спасибо за этот ответ!
CodyEngel
2

Используйте 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();
        }
    });
Кришан Кумар Мурья
источник
1

Прежде чем notifyDataSetChanged()просто проверить это с помощью этого метода:recyclerView.IsComputingLayout()

Амир Хоссейн Гасеми
источник
1

Простое использование сообщения:

new Handler().post(new Runnable() {
        @Override
        public void run() {
                mAdapter.notifyItemChanged(mAdapter.getItemCount() - 1);
            }
        }
    });
Кай Ван
источник
0

Я столкнулся именно с этой проблемой! После того, как ответ 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. Это просто перехватило исключение, но все же позволило моему адаптеру зарегистрировать изменение состояния и обновить отображение!

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

РЕДАКТИРОВАТЬ: Я признаю, что это, вероятно, не правильный / зрелый способ решения проблемы, но, поскольку он, похоже, не вызывает каких-либо проблем, оставляя исключение необработанным, я думал, что поделюсь, если это будет хорошо хватит для кого-то еще.

Андрей
источник
0

Это происходит потому, что вы, вероятно, устанавливаете «слушателя» до того, как настраиваете значение для этой строки, что заставляет слушателя запускаться, когда вы «настраиваете значение» для флажка.

Что вам нужно сделать:

@Override
public void onBindViewHolder(YourAdapter.ViewHolder viewHolder, int position) {
   viewHolder.mCheckBox.setOnCheckedChangeListener(null);
   viewHolder.mCheckBox.setChecked(trueOrFalse);
   viewHolder.setOnCheckedChangeListener(yourCheckedChangeListener);
}
Алесио Карвалью
источник
0
        @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 {

                    }

                }
            };
        }
Jayendrasinh Vaghela
источник
0

просто использовать isPressed()метод CompoundButtonв , onCheckedChanged(CompoundButton compoundButton, boolean isChecked)
например ,

public void onCheckedChanged(CompoundButton compoundButton, boolean isChecked) {   
                      ... //your functionality    
                            if(compoundButton.isPressed()){
                                notifyDataSetChanged();
                            }
                        }  });
Асад
источник
0

У меня была такая же проблема с использованием флажка и RadioButton. Замена notifyDataSetChanged()на notifyItemChanged(position)работала. Я добавил в isCheckedмодель данных логическое поле . Затем я обновил логическое значение и onCheckedChangedListenerпозвонил notifyItemChanged(adapterPosition). Возможно, это не лучший способ, но у меня сработало. Логическое значение используется для проверки, отмечен ли элемент.

Вишак А Каматх
источник
0

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

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

поэтому оберните весь код списка внутри метода isPressed. и все готово.

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

                if(compoundButton.isPressed()) {


                       //your code
                        notifyDataSetChanged();   

            }
        });
Сохам Пандья
источник
0

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

МОДЕЛЬ КЛАСС

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

}

Дивяншу Кумар
источник
-1

Для меня проблема возникла, когда я вышел из EditText с помощью Done, Back или внешнего касания ввода. Это приводит к обновлению модели вводимым текстом, а затем обновлению представления ресайклера с помощью наблюдения за данными в реальном времени.

Проблема заключалась в том, что курсор / фокус оставались в EditText.

Когда я удалил фокус, используя:

editText.clearFocus() 

Метод уведомления об изменении данных в представлении ресайклера перестал выдавать эту ошибку.

Думаю, это одна из возможных причин / решений данной проблемы. Не исключено, что это исключение можно исправить другим способом, так как оно может быть вызвано совершенно другой причиной.

Михал Зиобро
источник