Android RecyclerView: notifyDataSetChanged () IllegalStateException

130

Estou tentando atualizar os itens de uma revisão de reciclagem usando notifyDataSetChanged ().

Este é o meu método onBindViewHolder () no adaptador de reciclagem.

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

O que eu quero fazer é atualizar os itens da lista, depois de marcar uma caixa de seleção. Eu recebo uma exceção 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)

Eu também usei notifyItemChanged (), mesma exceção. Alguma maneira secreta de atualizar para notificar o adaptador de que algo mudou?

Arthur
fonte
Estou tendo esse mesmo problema agora. colocando o ouvinte setoncheckchanged na viewholder construtor dá-me o mesmo erro
filthy_wizard

Respostas:

145

Você deve mover o método 'setOnCheckedChangeListener ()' para ViewHolder, que é a classe interna do seu adaptador.

onBindViewHolder()não é um método que inicialize ViewHolder. Este método é a etapa de atualização de cada item do reciclador. Quando você ligar notifyDataSetChanged(), onBindViewHolder()será chamado como o número de cada item vezes.

Portanto, se você notifyDataSetChanged()inserir onCheckChanged()e inicializar checkBox onBindViewHolder(), receberá IllegalStateException por causa da chamada de método circular.

clique na caixa de seleção -> onCheckedChanged () -> notifyDataSetChanged () -> onBindViewHolder () -> definir caixa de seleção -> onChecked ...

Simplesmente, você pode corrigir isso colocando um sinalizador no adaptador.

tente isso,

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
fonte
Entendo, faz sentido. Desejo que a plataforma poderia prever tal comportamento simples e dar uma solução ao invés de ter que depender de bandeiras ..
Arthur
Não importa onde você define o ouvinte, desde que não notifique o AdapterViewObservertempo onBindViewHolder()em andamento.
Yaroslav Mytkalyk
6
Prefiro esta solução stackoverflow.com/a/32373999/1771194 com algumas melhorias nos comentários. Também me permitiu criar "RadioGroup" no RecyclerView.
Artem
como faço para obter a posição dos itens na minha lista?
Filthy_wizard
2
isso não funciona para mim no espectador. ainda recebe o erro de falha. Eu preciso mudar vars no arraylist. muito estranho. não tenho certeza de onde posso anexar o listiner.
Filthy_wizard 3/03/16
45

Você pode redefinir o ouvinte anterior antes de fazer alterações e não receberá essa exceção.

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
fonte
2
Boa resposta, mas é melhor não criar ouvinte em cada chamada onBindViewHolder. Faça como um campo.
Artem
1
Usar um campo é obviamente melhor, eu estava apenas dando um exemplo que funciona. Mas obrigado pelo aviso, vou atualizar a resposta.
JoniDS
1
Na verdade, preciso vincular um novo ouvinte a qualquer momento, porque o ouvinte precisa de uma variável de posição atualizada a cada vez. Portanto, esta é uma ótima resposta, portanto não preciso usar um manipulador.
Rock Lee
Essa é definitivamente a melhor maneira de fazê-lo, pois nunca é recomendável manter o estado global, recomendado pela resposta aceita ( stackoverflow.com/a/31069171/882251 ).
21418 Darwind
Mais simples !! Obrigado !!
DalveerSinghDaiya 29/03
39

Usar um Handlerpara adicionar itens e chamar a notify...()partir disso Handlercorrigiu o problema para mim.

cybergen
fonte
3
Essa é a resposta certa, você não pode alterar o item enquanto estiver configurando (chamando onBindViewHolder). Nesse caso, você tem que chamar notifyDataSetChanged no final do circuito atual chamando Handler.post ()
pjanecze
1
@ user1232726 Se você criar o Handler no thread principal, não precisará especificar um Looper (o padrão é o looper de threads de chamada). Então sim, este é o meu conselho. Caso contrário, você também pode especificar o Looper manualmente.
22616 cybergen
infelizmente, minhas caixas de seleção não ficam marcadas quando eu rolar para baixo e voltar novamente. vaia. lol
filthy_wizard
@ user1232726 procure uma resposta ou faça uma nova pergunta que descreva seu problema.
cybergen #
2
Eu desencorajaria fortemente essa resposta, pois essa é uma maneira hacky de resolver o problema. Quanto mais você faz isso, mais seu código se torna complexo de entender. Consulte a resposta de Moonsoo para entender o problema e a resposta do JoniDS para resolver o problema.
Kalpesh Patel
26

Não sei bem, mas também tive o mesmo problema. Eu resolvi isso usando onClickListneremcheckbox

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

Tente isso, isso pode ajudar!

jigar
fonte
1
Bom trabalho) MAS apenas no clique (se eu mover o widget de movimento lento (SwitchCompat), essa ação será perdida. Esse é o único problema #
Vlad 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
fonte
Suponho que você possa notificar facilmente o adaptador duas vezes.
Максим Петлюк
8

Quando você tem o erro de mensagem:

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

Simples, basta fazer o que causa a exceção em:

RecyclerView.post(new Runnable() {
    @Override
    public void run() {
        /** 
        ** Put Your Code here, exemple:
        **/
        notifyItemChanged(position);
    }
});
Antoine Draune
fonte
1
Isso funcionou para mim. Quer saber se há algum problema com esta solução?
Sayooj Valsan
7

Encontrei uma solução simples -

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

Aqui, criamos um executável para notificarItemChanged para uma posição quando a recyclerview estiver pronta para lidar com isso.

Rohan Kandwal
fonte
5

seu item CheckBox está alterando o drawable quando você liga notifyDataSetChanged();para que essa exceção ocorra. Tente ligar notifyDataSetChanged();na postagem da sua visualização. Por exemplo:

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

No começo, pensei que a resposta do Moonsoo (a resposta aceita) não funcionaria para mim porque não consigo inicializar o meu setOnCheckedChangeListener()no construtor ViewHolder porque preciso vinculá-lo toda vez para obter uma variável de posição atualizada. Mas levei muito tempo para perceber o que ele estava dizendo.

Aqui está um exemplo da "chamada de método circular" sobre a qual ele está falando:

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

O único problema com isso é que, quando precisamos inicializar o comutador para ser ligado ou desligado (do estado salvo passado, por exemplo), ele está chamando o ouvinte que pode chamar nofityItemRangeChangedquais chamadas onBindViewHoldernovamente. Você não pode ligar onBindViewHolderquando já está onBindViewHolder], porque não pode notifyItemRangeChangedse já estiver no meio de notificar que o intervalo de itens foi alterado.Mas eu só precisava atualizar a interface do usuário para ativá-la ou desativá-la, sem querer realmente disparar nada.

Aqui está a solução que aprendi com da resposta do JoniDS que impedirá o loop infinito. Desde que definamos o ouvinte como "nulo" antes de definir o Checked, ele atualizará a interface do usuário sem acionar o ouvinte, evitando o loop infinito. Então podemos definir o ouvinte depois.

Código do JoniDS:

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

Solução completa para o meu exemplo:

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
fonte
Você deve evitar inicializar o OnCheckedChangeListener repetidamente no onBindViewHolder (é necessário menos GC dessa maneira). Isso deve ser chamado onCreateViewHolder e você obtém a posição chamando holder.getAdapterPosition ().
desenvolvedor android
4

Por que não verificar o RecyclerView.isComputingLayout()estado da seguinte maneira?

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

Enquanto o item está sendo vinculado pelo gerenciador de layout, é muito provável que você esteja configurando o estado marcado da sua caixa de seleção, o que está acionando o retorno de chamada.

Obviamente, isso é um palpite, porque você não publicou o rastreamento de pilha completa.

Você não pode alterar o conteúdo do adaptador enquanto o RV está recalculando o layout. Você pode evitá-lo não chamando notifyDataSetChanged se o estado verificado do item for igual ao valor enviado no retorno de chamada (que será o caso se a chamada checkbox.setCheckedestiver acionando o retorno de chamada).

yigit
fonte
Obrigado @yigit! Meu problema não teve a ver com uma caixa de seleção, mas em uma situação mais complexa em que eu tive que notificar um item diferente no adaptador, mas estava tendo uma falha semelhante. Atualizei minha lógica de notificação para atualizar apenas quando os dados estão realmente mudando e resolvi minha falha. Portanto, minha nova regra com o RecyclerViews: não notifique que algo mudou quando nada mudou. Muito obrigado por esta resposta!
CodyEngel
2

Use onClickListner na caixa de seleção em vez de OnCheckedChangeListener, resolverá o 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
fonte
1

Antes de notifyDataSetChanged()verificar isso com este método:recyclerView.IsComputingLayout()

Amir Hossein Ghasemi
fonte
1

Post simples de uso:

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

Corri para este problema exato! Depois que a resposta de Moonsoo realmente não flutuou no meu barco, eu errei um pouco e encontrei uma solução que funcionou para mim.

Primeiro, aqui está um pouco do meu 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());
            }
        }
    });

Você notará que estou notificando especificamente o adaptador para a posição que estou mudando, em vez de todo o conjunto de dados como você está fazendo. Dito isto, embora eu não possa garantir que isso funcione para você, resolvi o problema envolvendo meunotifyItemChanged() chamada em um bloco de tentativa / captura. Isso simplesmente capturou a exceção, mas ainda permitiu que meu adaptador registrasse a alteração de estado e atualizasse a exibição!

Espero que isso ajude alguém!

EDIT: admito que provavelmente essa não é a maneira correta / madura de lidar com o problema, mas como ele não parece estar causando problemas ao deixar a exceção sem tratamento, pensei em compartilhar caso fosse bom o suficiente para outra pessoa.

Andrew
fonte
0

Isso está acontecendo porque você provavelmente está configurando o 'ouvinte' antes de configurar o valor dessa linha, o que faz com que o ouvinte seja acionado quando você 'configura o valor' para a caixa de seleção.

O que você precisa fazer é:

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

basta usar o isPressed()método de CompoundButtonpor onCheckedChanged(CompoundButton compoundButton, boolean isChecked)
exemplo

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

Eu tive o mesmo problema ao usar a caixa de seleção e o RadioButton. Substituindo notifyDataSetChanged()por notifyItemChanged(position)trabalhado. Eu adicionei um campo booleano isCheckedao modelo de dados. Atualizei o valor booleano e onCheckedChangedListener, em , liguei notifyItemChanged(adapterPosition). Esta pode não ser a melhor maneira, mas funcionou para mim. O valor booleano é usado para verificar se o item está marcado.

Vishak A Kamath
fonte
0

na maioria das vezes isso acontece porque a notificação é alterada chamando o evento de verificação da caixa de seleção e, nesse caso, novamente , a notificação é alterada .

Para resolvê-lo, basta marcar se a caixa de seleção está marcada programaticamente ou pressionada pelo usuário. existe o método isPressed para isso.

então envolva todo o código do listner dentro do método isPressed. e está feito.

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

                if(compoundButton.isPressed()) {


                       //your code
                        notifyDataSetChanged();   

            }
        });
Soham Pandya
fonte
0

Sofri com esse problema por hora e é assim que você pode corrigi-lo. Mas antes de começar, existem algumas condições para esta solução.

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

Caixa de seleção na classe ADAPTER

CheckBox cb;

CONSTRUTOR DE CLASSE ADAPTADOR E LISTA DE MODELOS

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 [Use onclick no 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
fonte
-1

Para mim, ocorreu um problema ao sair do EditText por Concluído, Voltar ou toque de entrada externo. Isso faz com que o modelo seja atualizado com o texto inserido e, em seguida, atualize a exibição do reciclador por meio da observação de dados ao vivo.

O problema era que o cursor / foco permanecia no EditText.

Quando eu apaguei o foco usando:

editText.clearFocus() 

O método de notificação de alteração de dados da exibição do reciclador parou de gerar esse erro.

Penso que esta é uma das possíveis razões / soluções para este problema. É possível que essa exceção possa ser corrigida de outra maneira, pois pode ser causada por uma razão totalmente diferente.

Michał Ziobro
fonte