Android RecyclerView: notifyDataSetChanged () IllegalStateException

130

Estoy tratando de actualizar los elementos de una vista de reciclaje usando notifyDataSetChanged ().

Este es mi método onBindViewHolder () en el adaptador de reciclaje.

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

Lo que quiero hacer es actualizar los elementos de la lista, después de marcar una casilla de verificación. Sin embargo, recibo una excepción ilegal:"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)

También usé notifyItemChanged (), la misma excepción. ¿Alguna forma secreta de actualizar para notificar al adaptador que algo cambió?

Arturo
fuente
Estoy teniendo este mismo problema ahora. poner el oyente setoncheckchanged en el constructor del marcador de vista me da el mismo error
filthy_wizard

Respuestas:

145

Debe mover el método 'setOnCheckedChangeListener ()' a ViewHolder, que es la clase interna en su adaptador.

onBindViewHolder()No es un método de inicialización ViewHolder. Este método es el paso de actualizar cada elemento de reciclaje. Cuando llame notifyDataSetChanged(), onBindViewHolder()se le llamará como el número de cada artículo veces.

Así que si usted notifyDataSetChanged()pone en onCheckChanged()e inicializar casilla de verificación onBindViewHolder(), obtendrá IllegalStateException debido a la llamada al método circular.

haga clic en la casilla de verificación -> onCheckedChanged () -> notifyDataSetChanged () -> onBindViewHolder () -> establezca la casilla de verificación -> onChecked ...

Simplemente, puede solucionar esto colocando una bandera en Adaptador.

prueba esto,

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
fuente
Ya veo, tiene sentido. Desearía que la plataforma prediga un comportamiento tan simple y ofrezca una solución en lugar de tener que depender de las banderas ...
Arthur
No importa dónde configure el oyente, siempre y cuando no notifique AdapterViewObservermientras está onBindViewHolder()en progreso.
Yaroslav Mytkalyk
66
Prefiero esta solución stackoverflow.com/a/32373999/1771194 con algunas mejoras en los comentarios. También me permitió hacer "RadioGroup" en RecyclerView.
Artem
¿Cómo obtengo la posición de los artículos en mi lista?
filthy_wizard
2
esto no funciona para mí en el visor. Todavía recibo el error de bloqueo. Necesito cambiar vars en la lista de arrays. muy extraño. No estoy muy seguro de dónde puedo adjuntar el listado.
filthy_wizard
45

Puede reiniciar el escucha anterior antes de realizar cambios y no obtendrá esta excepción.

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
fuente
2
Buena respuesta, pero es mejor no crear oyentes en cada llamada onBindViewHolder. Hazlo como un campo.
Artem
1
Por supuesto, usar un campo es mejor, solo estaba dando un ejemplo que funciona. Pero gracias por la advertencia, actualizaré la respuesta.
JoniDS
1
De todos modos, necesito vincular a un nuevo oyente cada vez, porque el oyente necesita una variable de posición actualizada cada vez. Esta es una gran respuesta, así que no tengo que usar un controlador.
Rock Lee
Esta es definitivamente la mejor manera de hacerlo, ya que nunca se recomienda mantener el estado global, lo que recomienda la respuesta aceptada ( stackoverflow.com/a/31069171/882251 ).
Darwind
¡El más simple! Gracias !!
DalveerSinghDaiya
39

El uso de un Handlerpara agregar elementos y llamar notify...()desde este me Handlersolucionó el problema.

cibergen
fuente
3
Esa es la respuesta correcta, no puede cambiar el elemento mientras está configurando (al llamar a OnBindViewHolder). En ese caso, debe llamar a notifyDataSetChanged al final del ciclo actual llamando a Handler.post ()
pjanecze
1
@ user1232726 Si crea el controlador en el subproceso principal, no tiene que especificar un Looper (el valor predeterminado es el subproceso de subprocesos de llamada). Entonces sí, este es mi consejo. De lo contrario, también puede especificar el Looper manualmente.
cybergen
desafortunadamente, mis casillas de verificación no permanecen marcadas cuando me desplazo hacia abajo y vuelvo a subir. abucheo. lol
filthy_wizard
@ user1232726 busque una respuesta o haga una nueva pregunta que describa su problema.
cybergen
2
Desalentaría encarecidamente esta respuesta, ya que esta es una forma hacky de resolver el problema. Cuanto más lo haga, su código se volverá más complejo de entender. Consulte la respuesta de Moonsoo para comprender el problema y la respuesta de JoniDS para resolver el problema.
Kalpesh Patel
26

No lo sé bien, pero también tuve el mismo problema. Resolví esto usando onClickListnerelcheckbox

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

¡Prueba esto, esto puede ayudar!

jigar
fuente
1
Buen trabajo) PERO solo al hacer clic (si muevo lento el widget (SwitchCompat), esta acción se perderá. Este es el único 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);
                }
            }
        });
    }
bruce
fuente
Supongo que puede notificar fácilmente al adaptador dos veces.
Максим Петлюк
8

Cuando tienes el mensaje de error:

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

Simple, solo haz lo que causa la excepción en:

RecyclerView.post(new Runnable() {
    @Override
    public void run() {
        /** 
        ** Put Your Code here, exemple:
        **/
        notifyItemChanged(position);
    }
});
Antoine Draune
fuente
1
Esto funcionó para mí. ¿Se pregunta si hay algún problema con esta solución?
Sayooj Valsan
7

Encontró una solución 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));
            }
        });
    }
}

Aquí creamos un ejecutable para notificar a ItemChanged para una posición cuando la vista de reciclaje está lista para manejarla.

Rohan Kandwal
fuente
5

su elemento CheckBox está cambiando dibujable cuando llama notifyDataSetChanged();para que se produzca esta excepción. Intente llamar notifyDataSetChanged();en la publicación de su vista. Por ejemplo:

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

Al principio pensé que la respuesta de Moonsoo (la respuesta aceptada) no funcionaría para mí porque no puedo inicializar mi setOnCheckedChangeListener()en el constructor ViewHolder porque necesito vincularlo cada vez para que obtenga una variable de posición actualizada. Pero me llevó mucho tiempo darme cuenta de lo que estaba diciendo.

Aquí hay un ejemplo de la "llamada al método circular" de la que está hablando:

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

El único problema con esto es que cuando necesitamos inicializar el interruptor para que se active o desactive (desde el estado guardado en el pasado, por ejemplo), está llamando al oyente, que podría llamar a nofityItemRangeChangedqué llamadas onBindViewHoldernuevamente. No puede llamar onBindViewHoldercuando ya está en onBindViewHolder], porque no puede hacerlo notifyItemRangeChangedsi ya está en medio de la notificación de que el rango de elementos ha cambiado. Pero solo necesitaba actualizar la interfaz de usuario para mostrarla activada o desactivada, no queriendo realmente desencadenar nada.

Aquí está la solución que aprendí de la respuesta de JoniDS que evitará el ciclo infinito. Siempre que configuremos el oyente como "nulo" antes de configurar Checked, actualizará la interfaz de usuario sin activar el oyente, evitando el bucle infinito. Entonces podemos configurar el oyente después.

Código de JoniDS:

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

Solución completa a mi ejemplo:

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
fuente
Debe evitar inicializar OnCheckedChangeListener una y otra vez en onBindViewHolder (se necesita menos GC de esta manera). Se supone que esto se debe llamar en onCreateViewHolder, y usted obtiene la posición llamando a holder.getAdapterPosition ().
Desarrollador de Android
4

¿Por qué no verificar el RecyclerView.isComputingLayout()estado de la siguiente manera?

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
fuente
2

Mientras el administrador de diseño vincula el elemento, es muy probable que esté configurando el estado marcado de su casilla de verificación, que está activando la devolución de llamada.

Por supuesto, esto es una conjetura porque no publicó el seguimiento completo de la pila.

No puede cambiar el contenido del adaptador mientras RV está recalculando el diseño. Puede evitarlo si no llama a notifyDataSetChanged si el estado comprobado del elemento es igual al valor enviado en la devolución de llamada (que será el caso si la llamada checkbox.setCheckedactiva la devolución de llamada).

yigit
fuente
Gracias @yigit! Mi problema no tenía que ver con una casilla de verificación, sino con una situación más compleja en la que tenía que notificar un elemento diferente en el adaptador, pero recibía un bloqueo similar. Actualicé mi lógica de notificación para actualizar solo cuando los datos realmente están cambiando y resolvió mi bloqueo. Entonces, mi nueva regla con RecyclerViews: no notifique que algo cambió cuando nada cambió. Muchas gracias por esta respuesta!
CodyEngel
2

Use onClickListner en la casilla de verificación en lugar de OnCheckedChangeListener, resolverá el 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();
        }
    });
Krishan Kumar Mourya
fuente
1

Antes de notifyDataSetChanged()comprobar eso con este método:recyclerView.IsComputingLayout()

Amir Hossein Ghasemi
fuente
1

Publicación de uso simple:

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

Me encontré con este problema exacto! Después de que la respuesta de Moonsoo realmente no hizo flotar mi bote, me equivoqué un poco y encontré una solución que funcionó para mí.

Primero, aquí hay algo de mi código:

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

Notarás que estoy notificando específicamente al adaptador la posición que estoy cambiando, en lugar de todo el conjunto de datos como lo estás haciendo. Dicho esto, aunque no puedo garantizar que esto funcione para usted, resolví el problema envolviendo mi notifyItemChanged()llamada en un bloque try / catch. ¡Esto simplemente captó la excepción, pero aún permitió que mi adaptador registrara el cambio de estado y actualizara la pantalla!

¡Espero que esto ayude a alguien!

EDITAR: Admito que probablemente esta no sea la forma adecuada / madura de manejar el problema, pero dado que no parece estar causando ningún problema al dejar la excepción sin controlar, pensé que compartiría en caso de que fuera bueno suficiente para alguien más.

Andrés
fuente
0

Esto sucede porque probablemente esté configurando el 'oyente' antes de configurar el valor para esa fila, lo que hace que el oyente se active cuando 'configura el valor' para la casilla de verificación.

Lo que debes hacer es:

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

simplemente use el isPressed()método de CompoundButtonpor onCheckedChanged(CompoundButton compoundButton, boolean isChecked)
ej.

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

Tuve el mismo problema al usar Checkbox y RadioButton. Sustitución notifyDataSetChanged()por notifyItemChanged(position)trabajado. Agregué un campo booleano isCheckedal modelo de datos. Luego actualicé el valor booleano y en onCheckedChangedListener, llamé notifyItemChanged(adapterPosition). Puede que esta no sea la mejor manera, pero funcionó para mí. El valor booleano se usa para verificar si el elemento está marcado.

Vishak A Kamath
fuente
0

la mayoría de las veces sucede porque notificar a un cambio de conjunto de datos llamando al evento de casilla de verificación cambiado y en ese caso nuevamente hay un cambio de conjunto de notificación .

para resolverlo, simplemente puede verificar que la casilla de verificación esté marcada programáticamente o que el usuario la haya presionado. Hay un método presionado para ello.

así que envuelva todo el código de lista dentro del método isPressed. Y está hecho.

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

                if(compoundButton.isPressed()) {


                       //your code
                        notifyDataSetChanged();   

            }
        });
Soham Pandya
fuente
0

Sufrí con este problema durante horas y así es como puedes solucionarlo. Pero antes de comenzar, hay algunas condiciones para esta solución.

CLASE DE MODELO

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

CAJA DE VERIFICACIÓN en CLASE DE ADAPTADOR

CheckBox cb;

ADAPTADOR CLASE CONSTRUCTOR Y LISTA DE MODELO

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 [Utilice onclick en lugar 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
fuente
-1

Para mí, el problema ocurrió cuando salí de EditText por Listo, Atrás o toque de entrada externo. Esto hace que se actualice el modelo con el texto de entrada, luego se actualiza la vista del reciclador mediante la observación de datos en vivo.

El problema era que el cursor / foco permanecía en EditText.

Cuando he eliminado el foco usando:

editText.clearFocus() 

El método de notificación de cambio de datos de la vista del reciclador dejó de arrojar este error.

Creo que esta es una de las posibles razones / soluciones a este problema. Es posible que esta excepción se pueda solucionar de otra manera, ya que puede ser causada por una razón totalmente diferente.

Michał Ziobro
fuente