Android RecyclerView: notifyDataSetChanged () IllegalStateException


130

Sto provando ad aggiornare gli elementi di un recycleview usando notifyDataSetChanged ().

Questo è il mio metodo onBindViewHolder () nell'adattatore 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();
        }
    });
}

Quello che voglio fare è aggiornare le voci dell'elenco, dopo aver spuntato una casella. Ricevo un'eccezione illegale però:"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)

Ho anche usato notifyItemChanged (), stessa eccezione. Qualche modo segreto per aggiornare per informare l'adattatore che qualcosa è cambiato?


sto avendo lo stesso problema ora. mettere il listoncheckchanged listener nel costruttore del viewholder mi dà lo stesso errore
filthy_wizard

Risposte:


145

È necessario spostare il metodo 'setOnCheckedChangeListener ()' su ViewHolder che è la classe interna sulla scheda.

onBindViewHolder()non è un metodo che si inizializza ViewHolder. Questo metodo consente di aggiornare ciascun articolo riciclatore. Quando chiami notifyDataSetChanged(), onBindViewHolder()verrà chiamato come il numero di ogni oggetto volte.

Quindi, se si notifyDataSetChanged()inserisce onCheckChanged()e si inizializza checkBox onBindViewHolder(), si otterrà IllegalStateException a causa della chiamata al metodo circolare.

fare clic sulla casella di controllo -> onCheckedChanged () -> notifyDataSetChanged () -> onBindViewHolder () -> impostare la casella di controllo -> onChecked ...

Semplicemente, puoi risolvere questo problema inserendo un flag in Adapter.

prova questo,

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

Capisco, ha senso. Vorrei che la piattaforma prevedesse un comportamento così semplice e fornisse una soluzione invece di dover fare affidamento sulle bandiere.
Arthur,

Non importa dove si imposta l'ascoltatore purché non si comunichi il AdapterViewObservertempo onBindViewHolder()in corso.
Yaroslav Mytkalyk,

6
Preferisco questa soluzione stackoverflow.com/a/32373999/1771194 con alcuni miglioramenti nei commenti. Mi ha anche permesso di creare "RadioGroup" in RecyclerView.
Artem

come ottengo la posizione degli articoli nella mia lista ??
filthy_wizard

2
questo non funziona per me nel viewholder. ottiene ancora l'errore di arresto anomalo. ho bisogno di cambiare vari nell'arraylist. molto strano. non sono sicuro di dove posso collegare il listiner.
filthy_wizard

45

Puoi semplicemente ripristinare il listener precedente prima di apportare modifiche e non otterrai questa eccezione.

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
Buona risposta, ma è meglio non creare listener su ogni chiamata onBindViewHolder. Fallo come un campo.
Artem

1
Usare un campo è ovviamente meglio, stavo solo dando un esempio che funziona. Ma grazie per l'avvertimento, aggiornerò la risposta.
JoniDS

1
In realtà ho comunque bisogno di associare un nuovo ascoltatore ogni volta, perché l'ascoltatore ha bisogno di una variabile di posizione aggiornata ogni volta. Quindi questa è un'ottima risposta, quindi non devo usare un gestore.
Rock Lee,

Questo è sicuramente il modo migliore per farlo poiché non è mai raccomandato di mantenere lo stato globale, che la risposta accettata ( stackoverflow.com/a/31069171/882251 ) sta raccomandando.
Darwind,

Quello più semplice !! Grazie !!
DalveerSinghDaiya

39

L'uso di a Handlerper aggiungere elementi e chiamare notify...()da questo Handlerrisolto il problema per me.


3
Questa è la risposta giusta, non è possibile modificare l'elemento durante l'impostazione (chiamando onBindViewHolder). In tal caso, devi chiamare notifyDataSetChanged alla fine del loop corrente chiamando Handler.post ()
pjanecze,

1
@ user1232726 Se si crea il gestore sul thread principale, non è necessario specificare un Looper (impostazione predefinita per il looper dei thread chiamanti). Quindi sì, questo è il mio consiglio. Altrimenti è anche possibile specificare manualmente il Looper.
cybergen,

purtroppo le mie caselle di controllo non rimangono selezionate quando scorro verso il basso e per scorrere nuovamente verso l'alto. boo. lol
filthy_wizard

@ user1232726 cercare una risposta o porre una nuova domanda che descriva il problema.
cybergen,

2
Vivamente scoraggiare questa risposta come questo è un modo hacky per risolvere il problema. Più fai questo più il tuo codice diventa complesso da capire. Consultare la risposta di Moonsoo per comprendere il problema e la risposta di JoniDS per risolvere il problema.
Kalpesh Patel,

26

Non lo so bene, ma ho avuto anche lo stesso problema. Ho risolto questo utilizzando onClickListnerilcheckbox

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

Prova questo, questo può aiutare!


1
Bel lavoro) MA solo con un clic (se muovo lentamente il widget (SwitchCompat), questa azione ci mancherà. Questo è l'unico problema
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);
                }
            }
        });
    }

Suppongo che tu possa facilmente notificare l'adattatore due volte.
Максим Петлюк,

8

Quando si verifica l'errore del messaggio:

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

Semplice, fai ciò che causa l'eccezione in:

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

1
Questo ha funzionato per me. Ti chiedi c'è qualche problema con questa soluzione?
Sayooj Valsan,

7

Trovato una soluzione semplice -

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

Qui creiamo un runnable per notificationItemChanged per una posizione quando recyclerview è pronto per gestirlo.


5

il tuo oggetto CheckBox sta cambiando drawable quando chiami, notifyDataSetChanged();quindi si verificherebbe questa eccezione. Prova a chiamare notifyDataSetChanged();per posta. Per esempio:

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

4

All'inizio ho pensato che la risposta di Moonsoo (la risposta accettata) non avrebbe funzionato per me perché non potevo inizializzare la mia setOnCheckedChangeListener()nel costruttore ViewHolder perché ho bisogno di associarla ogni volta in modo da ottenere una variabile di posizione aggiornata. Ma mi ci è voluto molto tempo per capire cosa stesse dicendo.

Ecco un esempio della "chiamata al metodo circolare" di cui sta parlando:

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

L'unico problema con questo è che quando dobbiamo inizializzare l'interruttore per essere acceso o spento (dallo stato salvato in passato, ad esempio), sta chiamando l'ascoltatore che potrebbe chiamare nofityItemRangeChangedquali chiamate di onBindViewHoldernuovo. Non puoi chiamare onBindViewHolderquando sei già onBindViewHolder], perché non puoi notifyItemRangeChangedse stai già notificando che l'intervallo di articoli è cambiato. Ma avevo solo bisogno di aggiornare l'interfaccia utente per mostrarla on o off, non volendo effettivamente innescare nulla.

Ecco la soluzione che ho imparato dalla risposta di JoniDS che impedirà il ciclo infinito. Finché impostiamo il listener su "null" prima di impostare Checked, aggiornerà l'interfaccia utente senza innescare il listener, evitando il ciclo infinito. Quindi possiamo impostare l'ascoltatore dopo.

Codice JoniDS:

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

Soluzione completa al mio esempio:

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

Dovresti evitare di inizializzare OnCheckedChangeListener ancora e ancora in onBindViewHolder (meno GC in questo modo necessario). Questo dovrebbe essere chiamato in onCreateViewHolder e ottieni la posizione chiamando holder.getAdapterPosition ().
sviluppatore Android il

4

Perché non controllare lo RecyclerView.isComputingLayout()stato come segue?

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

Mentre l'elemento è vincolato dal gestore del layout, è molto probabile che tu stia impostando lo stato selezionato della tua casella di controllo, che sta attivando la richiamata.

Ovviamente questa è un'ipotesi perché non hai pubblicato la traccia dello stack completo.

Non è possibile modificare il contenuto dell'adattatore mentre RV sta ricalcolando il layout. È possibile evitarlo non chiamando notificationDataSetChanged se lo stato verificato dell'elemento è uguale al valore inviato nel callback (che sarà il caso se call checkbox.setCheckedsta attivando il callback).


Grazie @yigit! Il mio problema non riguardava una casella di controllo, ma una situazione più complessa in cui dovevo notificare un elemento diverso nell'adattatore, ma stavo ottenendo un arresto simile. Ho aggiornato la mia logica di notifica per aggiornare solo quando i dati stanno effettivamente cambiando e ha risolto il mio arresto anomalo. Quindi la mia nuova regola con RecyclerViews: non avvisare che qualcosa è cambiato quando non è cambiato nulla. Mille grazie per questa risposta!
CodyEngel,

2

Usa onClickListner sulla casella di controllo invece di OnCheckedChangeListener, risolverà il problema

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

Posta uso semplice:

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

0

Ho riscontrato questo problema esatto! Dopo che la risposta di Moonsoo non ha fatto galleggiare la mia barca, ho fatto un po 'di casino e ho trovato una soluzione che ha funzionato per me.

Innanzitutto, ecco alcuni dei miei codici:

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

Noterai che sto specificatamente avvisando l'adattatore per la posizione che sto cambiando, invece dell'intero set di dati come stai facendo. Detto questo, anche se non posso garantire che funzionerà per te, ho risolto il problema avvolgendo la mia notifyItemChanged()chiamata in un blocco try / catch. Ciò ha semplicemente colto l'eccezione, ma ha comunque consentito al mio adattatore di registrare il cambio di stato e aggiornare il display!

Spero che questo aiuti qualcuno!

EDIT: Lo ammetto, questo probabilmente non è il modo corretto / maturo di gestire il problema, ma poiché non sembra causare alcun problema lasciando l'eccezione non gestita, ho pensato di condividere nel caso fosse buono abbastanza per qualcun altro.


0

Questo sta accadendo perché probabilmente stai impostando il 'listener' prima di configurare il valore per quella riga, il che fa sì che il listener venga attivato quando 'configuri il valore' per la casella di controllo.

Quello che devi fare è:

@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

utilizzare semplicemente isPressed()metodo CompoundButtonin onCheckedChanged(CompoundButton compoundButton, boolean isChecked)
es

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

0

Ho avuto lo stesso problema usando la casella di controllo e il RadioButton. Sostituzione notifyDataSetChanged()con notifyItemChanged(position)lavorato. Ho aggiunto un campo booleano isCheckedal modello di dati. Quindi ho aggiornato il valore booleano e in onCheckedChangedListener, ho chiamato notifyItemChanged(adapterPosition). Questo potrebbe non essere il modo migliore, ma ha funzionato per me. Il valore booleano viene utilizzato per verificare se l'elemento è controllato.


0

per lo più accade perché notifydatasetchanged chiama on Checkedchanged event of checkbox e in quel caso ancora non viene notifydatasetchanged .

per risolverlo puoi semplicemente controllare che la casella di controllo sia spuntata programmaticamente o che l'utente l'ha premuto. c'è un metodo isPressed per questo.

quindi avvolgi tutto il codice listener all'interno del metodo isPressed. ed è fatto.

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

                if(compoundButton.isPressed()) {


                       //your code
                        notifyDataSetChanged();   

            }
        });

0

Ho sofferto di questo problema per ora ed è così che puoi risolverlo. Ma prima di iniziare ci sono alcune condizioni per questa soluzione.

CLASSE MODELLO

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 in CLASSE ADATTATORE

CheckBox cb;

COSTRUTTORE DI CLASSE ADATTATORE ED ELENCO DEL MODELLO

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 [Utilizzare onclick al posto di 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

Per me il problema si è verificato quando sono uscito da EditText per Fine, Indietro o tocco esterno. Ciò provoca l'aggiornamento del modello con il testo di input, quindi l'aggiornamento della vista del riciclatore tramite l'osservazione dei dati in tempo reale.

Il problema era che il cursore / focus rimanevano in EditText.

Quando ho eliminato lo stato attivo utilizzando:

editText.clearFocus() 

Il metodo di notifica dei dati modificati della vista del riciclo ha smesso di generare questo errore.

Penso che questa sia una delle possibili ragioni / soluzioni a questo problema. È possibile che questa eccezione possa essere risolta in un altro modo in quanto può essere causata da una ragione totalmente diversa.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.