Android RecyclerView: notifyDataSetChanged () IllegalStateException


131

Próbuję zaktualizować elementy widoku recyklingu przy użyciu notifyDataSetChanged ().

To jest moja metoda onBindViewHolder () w adapterze recyklingowym.

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

Chcę zaktualizować pozycje na liście po zaznaczeniu pola wyboru. Mam jednak nielegalny wyjątek:"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)

Użyłem również notifyItemChanged (), ten sam wyjątek. Jakikolwiek tajny sposób aktualizacji w celu powiadomienia adaptera, że ​​coś się zmieniło?


mam teraz ten sam problem. umieszczenie detektora setoncheckchanged w konstruktorze viewholder daj mi ten sam błąd
filthy_wizard

Odpowiedzi:


147

Należy przenieść metodę „setOnCheckedChangeListener ()” do ViewHolder, która jest klasą wewnętrzną adaptera.

onBindViewHolder()nie jest metodą inicjującą ViewHolder. Ta metoda jest krokiem odświeżania każdego elementu recyklingu. Kiedy zadzwonisz notifyDataSetChanged(), onBindViewHolder()zostanie wywołany jako liczba razy każdej pozycji.

Więc jeśli notifyDataSetChanged()wstawisz onCheckChanged()checkBox i zainicjujesz go onBindViewHolder(), otrzymasz IllegalStateException z powodu cyklicznego wywołania metody.

kliknij pole wyboru -> onCheckedChanged () -> notifyDataSetChanged () -> onBindViewHolder () -> ustaw pole wyboru -> onChecked ...

Po prostu możesz to naprawić, umieszczając jedną flagę w adapterze.

Spróbuj tego,

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

Rozumiem, ma sens. Chciałbym, żeby platforma przewidziała takie proste zachowanie i dała rozwiązanie zamiast polegać na flagach ...
Arthur

Nie ma znaczenia, gdzie można ustawić słuchacza tak długo, jak nie powiadamia AdapterViewObserver, gdy onBindViewHolder()w toku.
Yaroslav Mytkalyk

6
Wolę to rozwiązanie stackoverflow.com/a/32373999/1771194 z pewnymi ulepszeniami w komentarzu. Pozwoliło mi to również na utworzenie „RadioGroup” w RecyclerView.
Artem,

jak uzyskać pozycję pozycji na mojej liście?
filthy_wizard

2
to nie działa dla mnie w widoku. nadal pojawia się błąd awarii. muszę zmienić vars w arraylist. bardzo dziwny. nie wiem, gdzie mogę dołączyć listiner.
filthy_wizard

46

Możesz po prostu zresetować poprzedniego odbiornika przed wprowadzeniem zmian i nie otrzymasz tego wyjątku.

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

2
Dobra odpowiedź, ale lepiej nie tworzyć odbiornika przy każdym wywołaniu onBindViewHolder. Zrób to jako pole.
Artem,

1
Korzystanie z pola jest oczywiście lepsze, podałem tylko przykład, który działa. Ale dzięki za ostrzeżenie zaktualizuję odpowiedź.
JoniDS,

1
Właściwie to i tak muszę wiązać nowego słuchacza za każdym razem, ponieważ słuchacz za każdym razem potrzebuje zaktualizowanej zmiennej pozycji. To świetna odpowiedź, więc nie muszę używać Handler.
Rock Lee

Jest to zdecydowanie najlepszy sposób na zrobienie tego, ponieważ nigdy nie zaleca się utrzymywania stanu globalnego, co zaleca zaakceptowana odpowiedź ( stackoverflow.com/a/31069171/882251 ).
Darwind,

Najprostszy !! Dzięki !!
DalveerSinghDaiya

39

Używanie a Handlerdo dodawania przedmiotów i dzwonienia notify...()z tego Handlerrozwiązania rozwiązało problem.


3
To właściwa odpowiedź, nie możesz zmienić elementu podczas ustawiania (z wywołaniem onBindViewHolder). W takim przypadku musisz wywołać notifyDataSetChanged na końcu bieżącej pętli, wywołując Handler.post ()
pjanecze

1
@ user1232726 Jeśli tworzysz Handler w głównym wątku, nie musisz określać Looper (domyślnie jest to looper wątków wywołujących). Więc tak, to moja rada. W przeciwnym razie możesz również określić Looper ręcznie.
cybergen

niestety moje pola wyboru nie pozostają zaznaczone, gdy przewijam w dół i ponownie przewijam w górę. gwizd. lol
filthy_wizard

@ user1232726 wyszukaj odpowiedź lub zadaj nowe pytanie opisujące Twój problem.
cybergen

2
Zdecydowanie odradzam tę odpowiedź, ponieważ jest to zmyślony sposób rozwiązania problemu. Im więcej robisz, tym bardziej Twój kod staje się trudniejszy do zrozumienia. Zapoznaj się z odpowiedzią Moonsoo, aby zrozumieć problem, i odpowiedzią JoniDS, aby rozwiązać problem.
Kalpesh Patel

26

Nie wiem dobrze, ale miałem ten sam problem. Rozwiązałem to, używając onClickListneroncheckbox

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

Spróbuj tego, to może pomóc!


1
Dobra robota), ALE tylko po kliknięciu (jeśli mam widżet wolnego ruchu (SwitchCompat), ta akcja zostanie pominięta.To jedyny problem
Vlad

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

Przypuszczam, że bez problemu można dwukrotnie powiadomić adapter.
Максим Петлюк

8

Znalazłem proste rozwiązanie -

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

Tutaj tworzymy runnable do notificationItemChanged dla pozycji, gdy recyclinglerview jest gotowy do obsługi.


jest to lepsze rozwiązanie niż to oznaczone jako prawidłowe.
Miguel Silva

8

Gdy pojawi się komunikat o błędzie:

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

Proste, po prostu zrób to, co powoduje wyjątek w:

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

1
To zadziałało dla mnie. Zastanawiasz się, czy jest jakiś problem z tym rozwiązaniem?
Sayooj Valsan

5

Na początku myślałem, że odpowiedź Moonsoo (zaakceptowana odpowiedź) nie zadziała dla mnie, ponieważ nie mogę zainicjować mojego setOnCheckedChangeListener()w konstruktorze ViewHolder, ponieważ muszę go wiązać za każdym razem, aby uzyskać zaktualizowaną zmienną pozycji. Ale zajęło mi dużo czasu, zanim zrozumiałem, o czym mówi.

Oto przykład „okrągłego wywołania metody”, o którym mówi:

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

Jedynym problemem jest to, że kiedy musimy zainicjować przełącznik, aby był włączony lub wyłączony (na przykład z poprzedniego stanu zapisanego), jest to wywołanie słuchacza, który może zadzwonić, nofityItemRangeChangedktóry dzwoni onBindViewHolderponownie. Nie możesz zadzwonić, onBindViewHolderkiedy już jesteś w środku onBindViewHolder], ponieważ nie możesz, notifyItemRangeChangedjeśli jesteś już w trakcie powiadamiania o zmianie zakresu przedmiotów. Ale musiałem tylko zaktualizować interfejs użytkownika, aby go włączyć lub wyłączyć, nie chcąc niczego uruchamiać.

Oto rozwiązanie, którego nauczyłem się z odpowiedzi JoniDS, które zapobiegnie nieskończonej pętli.Tak długo, jak ustawimy odbiornik na „null” przed ustawieniem Checked, wówczas zaktualizuje on interfejs użytkownika bez wyzwalania detektora, unikając nieskończonej pętli. Następnie możemy ustawić słuchacza po.

Kod JoniDS:

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

Pełne rozwiązanie mojego przykładu:

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

Należy unikać wielokrotnego inicjowania OnCheckedChangeListener w onBindViewHolder (w ten sposób potrzeba mniej GC). To powinno być wywoływane w onCreateViewHolder, a pozycję uzyskuje się, wywołując funkcję holder.getAdapterPosition ().
programista Androida

5

Twój element CheckBox zmienia się do rysowania po wywołaniu, notifyDataSetChanged();więc ten wyjątek wystąpiłby. Spróbuj zadzwonić notifyDataSetChanged();pocztą swojego widoku. Na przykład:

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

5

Dlaczego nie sprawdzić RecyclerView.isComputingLayout()stanu w następujący sposób?

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

2

Podczas gdy element jest wiązany przez menedżera układu, jest bardzo prawdopodobne, że ustawiasz zaznaczony stan swojego pola wyboru, który uruchamia wywołanie zwrotne.

Oczywiście to przypuszczenie, ponieważ nie opublikowałeś pełnego śladu stosu.

Nie można zmienić zawartości adaptera, gdy RV ponownie oblicza układ. Możesz tego uniknąć, nie wywołując notifyDataSetChanged, jeśli stan zaznaczenia elementu jest równy wartości wysłanej w wywołaniu zwrotnym (co będzie miało miejsce w przypadku wywołania checkbox.setCheckedwywołania zwrotnego).


Dzięki @yigit! Mój problem nie dotyczył pola wyboru, ale bardziej złożonej sytuacji, w której musiałem powiadomić inny element w adapterze, ale otrzymywałem podobną awarię. Zaktualizowałem logikę powiadomień, aby aktualizować się tylko wtedy, gdy dane faktycznie się zmieniają, i rozwiązało to moją awarię. Więc moja nowa reguła z RecyclerViews: nie powiadamiaj, że coś się zmieniło, gdy nic się nie zmieniło. Wielkie dzięki za tę odpowiedź!
CodyEngel

2

Użyj onClickListner na polu wyboru zamiast OnCheckedChangeListener, rozwiąże to problem

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

Proste użycie Post:

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

1

Cierpiałem z tym problemem przez godzinę i tak można to naprawić. Ale zanim zacząłeś, istnieją pewne warunki dotyczące tego rozwiązania.

KLASA MODELOWA

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 W KLASIE ADAPTERÓW

CheckBox cb;

KONSTRUKTOR KLASY ADAPTERÓW I LISTA MODELI

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 [Użyj onclick zamiast 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();
        }
    });

}


Wow, to najlepsza odpowiedź.
Chirag Prajapati

0

Natknąłem się dokładnie na ten problem! Po tym, jak odpowiedź Moonsoo tak naprawdę nie unosiła mojej łodzi na wodzie, trochę się pogubiłem i znalazłem rozwiązanie, które działało dla mnie.

Po pierwsze, oto część mojego kodu:

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

Zauważysz, że specjalnie powiadamiam adapter o zmienianej pozycji, zamiast o całym zestawie danych, tak jak robisz. Biorąc to pod uwagę, chociaż nie mogę zagwarantować, że to zadziała, rozwiązałem problem, zawijając moje notifyItemChanged()wywołanie blokiem try / catch. To po prostu złapało wyjątek, ale nadal pozwalało mojemu adapterowi zarejestrować zmianę stanu i zaktualizować wyświetlacz!

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

EDYCJA: Przyznaję, że prawdopodobnie nie jest to właściwy / dojrzały sposób rozwiązania problemu, ale ponieważ nie wydaje się, aby powodował jakiekolwiek problemy, pozostawiając wyjątek bez obsługi, pomyślałem, że podzielę się, na wypadek, gdyby był dobry wystarczy dla kogoś innego.


0

Dzieje się tak, ponieważ prawdopodobnie ustawiasz „detektor” przed skonfigurowaniem wartości w tym wierszu, co powoduje, że detektor zostanie wyzwolony, gdy „skonfigurujesz wartość” dla pola wyboru.

Musisz:

@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 {

                    }

                }
            };
        }

0

wystarczy użyć isPressed()metody CompoundButtonna onCheckedChanged(CompoundButton compoundButton, boolean isChecked)
przykład

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

0

Miałem ten sam problem, używając pola wyboru i przycisku RadioButton. Wymiana notifyDataSetChanged()z notifyItemChanged(position)działało. Dodałem pole Boolean isCheckeddo modelu danych. Następnie zaktualizowałem wartość logiczną i onCheckedChangedListenerzadzwoniłem notifyItemChanged(adapterPosition). To może nie jest najlepszy sposób, ale dla mnie zadziałał. Wartość logiczna służy do sprawdzania, czy element jest zaznaczony.


0

przeważnie dzieje się tak, ponieważ notifydatasetchanged wywołanie onCheckedzmienione zdarzenie pola wyboru iw tym przypadku ponownie jest notifydatasetchanged .

aby go rozwiązać, wystarczy zaznaczyć, że pole wyboru jest zaznaczone przez programowe lub naciśnięte przez użytkownika. istnieje metoda isPressed dla niego.

więc zawiń cały kod listnera wewnątrz metody isPressed. i gotowe.

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

                if(compoundButton.isPressed()) {


                       //your code
                        notifyDataSetChanged();   

            }
        });

-1

U mnie problem wystąpił, gdy wyszedłem z EditText przez Gotowe, Wstecz lub zewnętrzne dotknięcie. Powoduje to aktualizację modelu za pomocą tekstu wejściowego, a następnie odświeżenie widoku recyklera poprzez obserwację danych na żywo.

Problem polegał na tym, że kursor / fokus pozostały w EditText.

Kiedy usunąłem fokus za pomocą:

editText.clearFocus() 

Powiadamiaj, że metoda widoku recyklera danych została zmieniona, przestała zgłaszać ten błąd.

Myślę, że jest to jeden z możliwych powodów / rozwiązań tego problemu. Możliwe, że ten wyjątek można naprawić w inny sposób, ponieważ może to wynikać z zupełnie innego powodu.

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.