Android RecyclerView: notifyDataSetChanged () IllegalStateException

130

J'essaye de mettre à jour les éléments d'un recycleview en utilisant notifyDataSetChanged ().

C'est ma méthode onBindViewHolder () dans l'adaptateur de 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();
        }
    });
}

Ce que je veux faire, c'est mettre à jour les éléments de la liste, après avoir coché une case. J'obtiens cependant une exception illégale:"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)

J'ai également utilisé notifyItemChanged (), même exception. Un moyen secret de mettre à jour pour informer l'adaptateur que quelque chose a changé?

Arthur
la source
Im ayant ce même problème maintenant. mettre l'écouteur setoncheckchanged dans le constructeur de la visionneuse me donne la même erreur
filthy_wizard

Réponses:

145

Vous devez déplacer la méthode 'setOnCheckedChangeListener ()' vers ViewHolder qui est la classe interne de votre adaptateur.

onBindViewHolder()n'est pas une méthode qui s'initialise ViewHolder. Cette méthode est une étape d'actualisation de chaque article recycleur. Lorsque vous appelez notifyDataSetChanged(), onBindViewHolder()sera appelé le nombre de fois de chaque élément.

Donc, si vous notifyDataSetChanged()insérez onCheckChanged()et initialisez checkBox onBindViewHolder(), vous obtiendrez IllegalStateException en raison de l'appel de méthode circulaire.

cliquez sur la case à cocher -> onCheckedChanged () -> notifyDataSetChanged () -> onBindViewHolder () -> cocher la case -> onChecked ...

Simplement, vous pouvez résoudre ce problème en mettant un indicateur dans l'adaptateur.

essaye ça,

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
la source
Je vois, ça a du sens. Je souhaite que la plate-forme prédise un comportement aussi simple et donne une solution au lieu de devoir compter sur des drapeaux ..
Arthur
Peu importe où vous définissez l'auditeur tant que vous ne notifiez pas le AdapterViewObservertemps onBindViewHolder()en cours.
Yaroslav Mytkalyk le
6
Je préfère cette solution stackoverflow.com/a/32373999/1771194 avec quelques améliorations en commentaire. Cela m'a aussi permis de créer "RadioGroup" dans RecyclerView.
Artem
comment puis-je obtenir la position des articles dans ma liste?
filthy_wizard
2
cela ne fonctionne pas pour moi dans le visualiseur. obtenez toujours l'erreur de plantage. j'ai besoin de changer de vars dans l'arraylist. très étrange. pas trop sûr où je peux joindre le listiner.
filthy_wizard
45

Vous pouvez simplement réinitialiser l'écouteur précédent avant d'apporter des modifications et vous n'obtiendrez pas cette exception.

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
la source
2
Bonne réponse, mais il vaut mieux ne pas créer d'écouteur à chaque appel onBindViewHolder. Faites-en un champ.
Artem
1
Utiliser un champ est bien sûr mieux, je donnais juste un exemple qui fonctionne. Mais merci pour l'avertissement, je mettrai à jour la réponse.
JoniDS
1
De toute façon, j'ai besoin de lier un nouvel écouteur à chaque fois, car l'auditeur a besoin d'une variable de position mise à jour à chaque fois. C'est donc une excellente réponse, donc je n'ai pas besoin d'utiliser un gestionnaire.
Rock Lee
C'est certainement la meilleure façon de le faire car il n'est jamais recommandé de conserver l'état global, ce que la réponse acceptée ( stackoverflow.com/a/31069171/882251 ) recommande.
Darwind
Le plus simple !! Merci !!
DalveerSinghDaiya
39

L'utilisation d'un Handlerpour ajouter des éléments et appeler à notify...()partir de cela a Handlerrésolu le problème pour moi.

Cybergen
la source
3
C'est la bonne réponse, vous ne pouvez pas changer d'élément pendant son paramétrage (en appelant onBindViewHolder). Dans ce cas, vous devez appeler notifyDataSetChanged à la fin de la boucle en cours en appelant Handler.post ()
pjanecze
1
@ user1232726 Si vous créez le Handler sur le thread principal, vous n'avez pas à spécifier de Looper (par défaut, le Looper des threads appelants). Alors oui, c'est mon conseil. Sinon, vous pouvez également spécifier le Looper manuellement.
cybergen
Malheureusement, mes cases à cocher ne restent pas cochées lorsque je fais défiler vers le bas et que je reviens en arrière. huer. lol
filthy_wizard
@ user1232726 recherchez une réponse ou posez une nouvelle question décrivant votre problème.
cybergen
2
Je déconseillerais fortement cette réponse car il s'agit d'une manière piratée de résoudre le problème. Plus vous faites cela, plus votre code devient complexe à comprendre. Référez- vous à la réponse de Moonsoo pour comprendre le problème et à la réponse de JoniDS pour résoudre le problème.
Kalpesh Patel
26

Je ne sais pas bien, mais j'ai aussi eu le même problème. J'ai résolu ce problème en utilisant onClickListnersurcheckbox

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

Essayez ceci, cela peut vous aider!

jigare
la source
1
Beau travail) MAIS seulement sur clic (si je ralentis le widget (SwitchCompat), cette action sera manquée.C'est le seul problème
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);
                }
            }
        });
    }
Bruce
la source
Je suppose que vous pouvez facilement notifier l'adaptateur deux fois.
Максим Петлюк
8

Lorsque vous avez l'erreur de message:

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

Simple, faites simplement ce qui cause l'exception dans:

RecyclerView.post(new Runnable() {
    @Override
    public void run() {
        /** 
        ** Put Your Code here, exemple:
        **/
        notifyItemChanged(position);
    }
});
Antoine Draune
la source
1
Cela a fonctionné pour moi. Vous vous demandez s'il y a un problème avec cette solution?
Sayooj Valsan
7

Trouvé une solution simple -

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

Ici, nous créons un exécutable à notifyItemChanged pour une position lorsque recyclerview est prêt à le gérer.

Rohan Kandwal
la source
5

votre élément CheckBox est en train de changer drawable lorsque vous appelez, notifyDataSetChanged();donc cette exception se produit. Essayez d'appeler notifyDataSetChanged();en post de votre vue. Par exemple:

buttonView.post(new Runnable() {
                    @Override
                    public void run() {
                        notifyDataSetChanged();
                    }
                });
Mohammad Reza Norouzi
la source
4

Au début, je pensais que la réponse de Moonsoo (la réponse acceptée) ne fonctionnerait pas pour moi car je ne peux pas initialiser mon setOnCheckedChangeListener()dans le constructeur ViewHolder car je dois le lier à chaque fois pour qu'il obtienne une variable de position mise à jour. Mais il m'a fallu beaucoup de temps pour réaliser ce qu'il disait.

Voici un exemple de "l'appel de méthode circulaire" dont il parle:

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

Le seul problème avec cela, c'est que lorsque nous avons besoin d'initialiser le commutateur pour qu'il soit activé ou désactivé (à partir de l'état sauvegardé passé, par exemple), il appelle l'auditeur qui pourrait appeler nofityItemRangeChangedqui appelle à onBindViewHoldernouveau. Vous ne pouvez pas appeler onBindViewHolderlorsque vous êtes déjà dans onBindViewHolder], car vous ne pouvez pas notifyItemRangeChangedsi vous êtes déjà en train de signaler que la gamme d'articles a changé. Mais je n'avais besoin que de mettre à jour l'interface utilisateur pour l'afficher ou la désactiver, ne voulant en fait rien déclencher.

Voici la solution que j'ai apprise de la réponse de JoniDS qui empêchera la boucle infinie. Tant que nous définissons l'auditeur sur "null" avant de définir Checked, il mettra à jour l'interface utilisateur sans déclencher l'auditeur, évitant ainsi la boucle infinie. Ensuite, nous pouvons définir l'auditeur après.

Code de JoniDS:

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

Solution complète à mon exemple:

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());
            }
        }
    });
}
Rock Lee
la source
Vous devez éviter d'initialiser le OnCheckedChangeListener encore et encore dans le onBindViewHolder (moins GC nécessaire de cette façon). Ceci est censé être appelé dans onCreateViewHolder, et vous obtenez la position en appelant holder.getAdapterPosition ().
développeur android
4

Pourquoi ne pas vérifier l' RecyclerView.isComputingLayout()état comme suit?

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
la source
2

Alors que l'élément est lié par le gestionnaire de mise en page, il est très probable que vous définissiez l'état coché de votre case à cocher, ce qui déclenche le rappel.

Bien sûr, c'est une supposition car vous n'avez pas publié la trace complète de la pile.

Vous ne pouvez pas modifier le contenu de l'adaptateur pendant que RV recalcule la disposition. Vous pouvez l'éviter en n'appelant pas notifyDataSetChanged si l'état vérifié de l'élément est égal à la valeur envoyée dans le rappel (ce qui sera le cas si l'appel checkbox.setCheckeddéclenche le rappel).

yigit
la source
Merci @yigit! Mon problème n'était pas lié à une case à cocher mais à une situation plus complexe où je devais notifier un élément différent de l'adaptateur, mais j'obtenais un plantage similaire. J'ai mis à jour ma logique de notification pour ne mettre à jour que lorsque les données changent réellement et que mon plantage a été résolu. Donc, ma nouvelle règle avec RecyclerViews: ne pas notifier que quelque chose a changé quand rien n'a changé. Merci beaucoup pour cette réponse!
CodyEngel
2

Utilisez onClickListner sur la case à cocher au lieu de OnCheckedChangeListener, cela résoudra le problème

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();
        }
    });
Krishan Kumar Mourya
la source
1

Avant de notifyDataSetChanged()simplement vérifier cela avec cette méthode:recyclerView.IsComputingLayout()

Amir Hossein Ghasemi
la source
1

Utilisation simple Post:

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

J'ai rencontré ce problème exact! Après que la réponse de Moonsoo n'ait pas vraiment fait flotter mon bateau, j'ai un peu dérangé et j'ai trouvé une solution qui a fonctionné pour moi.

Tout d'abord, voici une partie de mon code:

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

Vous remarquerez que je notifie spécifiquement à l'adaptateur la position que je change, au lieu de l'ensemble de données comme vous le faites. Cela étant dit, bien que je ne puisse pas garantir que cela fonctionnera pour vous, j'ai résolu le problème en enveloppant mon notifyItemChanged()appel dans un bloc try / catch. Cela a simplement attrapé l'exception, mais a quand même permis à mon adaptateur d'enregistrer le changement d'état et de mettre à jour l'affichage!

J'espère que cela aide quelqu'un!

EDIT: J'admets que ce n'est probablement pas la manière appropriée / mature de gérer le problème, mais comme cela ne semble pas causer de problèmes en laissant l'exception non gérée, j'ai pensé partager au cas où ce serait bien assez pour quelqu'un d'autre.

Andrew
la source
0

Cela se produit parce que vous définissez probablement l '«écouteur» avant de configurer la valeur de cette ligne, ce qui fait que l'écouteur est déclenché lorsque vous «configurez la valeur» pour la case à cocher.

Ce que vous devez faire est:

@Override
public void onBindViewHolder(YourAdapter.ViewHolder viewHolder, int position) {
   viewHolder.mCheckBox.setOnCheckedChangeListener(null);
   viewHolder.mCheckBox.setChecked(trueOrFalse);
   viewHolder.setOnCheckedChangeListener(yourCheckedChangeListener);
}
Alécio Carvalho
la source
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
la source
0

utilisez simplement la isPressed()méthode de CompoundButtonpar onCheckedChanged(CompoundButton compoundButton, boolean isChecked)
exemple

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

J'ai eu le même problème en utilisant la case à cocher et le RadioButton. Remplacer notifyDataSetChanged()par notifyItemChanged(position)travaillé. J'ai ajouté un champ booléen isCheckedau modèle de données. Ensuite, j'ai mis à jour la valeur booléenne et dans onCheckedChangedListener, j'ai appelé notifyItemChanged(adapterPosition). Ce n'est peut-être pas le meilleur moyen, mais cela a fonctionné pour moi. La valeur booléenne est utilisée pour vérifier si l'élément est vérifié.

Vishak A Kamath
la source
0

la plupart du temps, cela se produit parce que notifydatasetchanged appelle l' événement onCheckedchanged de la case à cocher et dans cet événement encore il y a notifydatasetchanged .

pour le résoudre, vous pouvez simplement vérifier que la case est cochée par programme ou que l'utilisateur l'a pressée. il y a la méthode IsPressed pour elle.

alors enveloppez tout le code listner dans la méthode isPressed. et c'est fait.

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

                if(compoundButton.isPressed()) {


                       //your code
                        notifyDataSetChanged();   

            }
        });
Soham Pandya
la source
0

J'ai souffert de ce problème pendant des heures et c'est ainsi que vous pouvez le résoudre. Mais avant de commencer, il y a certaines conditions à cette solution.

CLASSE DE MODÈLE

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 dans ADAPTER CLASS

CheckBox cb;

CONSTRUCTEUR DE CLASSE D'ADAPTATEUR ET LISTE DES MODÈLES

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 [Veuillez utiliser onclick au lieu de 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();
        }
    });

}

Divyanshu Kumar
la source
-1

Pour moi, un problème est survenu lorsque j'ai quitté EditText par Terminé, Retour ou touche d'entrée extérieure. Cela entraîne la mise à jour du modèle avec le texte d'entrée, puis l'actualisation de la vue du recycleur via l'observation des données en direct.

Le problème était que le curseur / focus reste dans EditText.

Lorsque j'ai supprimé le focus en utilisant:

editText.clearFocus() 

Notifier que la méthode d'affichage de l'outil de recyclage des données a changé a cessé de générer cette erreur.

Je pense que c'est l'une des raisons / solutions possibles à ce problème. Il est possible que cette exception puisse être corrigée d'une autre manière car elle peut être causée par une raison totalement différente.

Michał Ziobro
la source