質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

87.35%

RecyclerView.AdapterのonBindViewHolder内でnotifyDataSetChangedに対するエラーの回避策について

解決済

回答 1

投稿 編集

  • 評価
  • クリップ 0
  • VIEW 2,872

score 333

前提・実現したいこと

RecyclerView.AdapterのonBindViewHolder内でnotifyDataSetChangedに対するエラーの回避策について知りたいです。
Collections.sort(AlarmList)で、RecyclerViewに設定するArrayListの並び順がスイッチを押されることで変わることは確認しました。
その後、リサイクルビュー更新「notifyDataSetChanged()」で更新しようとしたところ、エラーが発生しました。

以下のページに同様の事象が報告されています。
Android RecyclerView : notifyDataSetChanged() IllegalStateException
具体的にどのように記述して対応すれば良いのでしょうか?

発生している問題・エラーメッセージ

E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.a.b, PID: 10998
    java.lang.IllegalStateException: Cannot call this method while RecyclerView is computing a layout or scrolling android.support.v7.widget.RecyclerView{cf244b5 VFED..... .F....ID 0,125-1080,1727 #7f08006e app:id/lv_alarm}, adapter:com.a.b.AlarmsAdapter@d14974a, layout:android.support.v7.widget.LinearLayoutManager@51926bb, context:com.a.b.MainActivity@c1f5cf9
        at android.support.v7.widget.RecyclerView.assertNotInLayoutOrScroll(RecyclerView.java:2880)
        at android.support.v7.widget.RecyclerView$RecyclerViewDataObserver.onChanged(RecyclerView.java:5281)
        at android.support.v7.widget.RecyclerView$AdapterDataObservable.notifyChanged(RecyclerView.java:11997)
        at android.support.v7.widget.RecyclerView$Adapter.notifyDataSetChanged(RecyclerView.java:7070)
        at com.a.b.AlarmsAdapter.alarm_switch_upd(AlarmsAdapter.java:200)

該当のソースコード

public class AlarmsAdapter extends RecyclerView.Adapter<AlarmsAdapter.ItemViewHolder> {
・・・
    @Override
    public void onBindViewHolder(final ItemViewHolder holder, final int position) {
        //行の値設定
        holder.sw_alarm.setChecked(AlarmList.get(position).m_sw_alarm);
        holder.t_gozengogo.setText(AlarmList.get(position).m_t_gozengogo);
        holder.t_time.setText(AlarmList.get(position).m_t_time);
        holder.t_ampm.setText(AlarmList.get(position).m_t_ampm);
        holder.t_alarm_name.setText(AlarmList.get(position).m_t_alarm_name);
        holder.t_week.setText(AlarmList.get(position).m_t_week);

        //アラームスイッチ変更
        holder.sw_alarm.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                if(holder.sw_alarm.isChecked()) {
                    //mSwitch : Off -> On の時の処理

                    // コンテキスト取得
                    context = buttonView.getContext();
                    //アラームスイッチ更新
                    alarm_switch_upd(holder.sw_alarm.isChecked() ,position);
                } else {
                    //mSwitch : On -> Off の時の処理

                    // コンテキスト取得
                    context = buttonView.getContext();
                    //アラームスイッチ更新
                    alarm_switch_upd(holder.sw_alarm.isChecked() ,position);
                }
                //アーリーリスト再ソート
                Collections.sort(AlarmList);

                //リサイクルビュー更新
                notifyDataSetChanged();
            }
        });
    }

補足情報(FW/ツールのバージョンなど)

Android Studio3.4
API14から28対象ターゲット28

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 1

checkベストアンサー

+1

notifyDataSetChanged によって RecyclerList が再表示を行おうと 全データに対し onBindViewHolder を呼び出し, スイッチを On にした行で setChecked を実行した際に そのスイッチに残っている OnCheckedChangeListener によって再度 notifyDataSetChanged が呼ばれ... と無限ループするので, 例外が発生するということですね.

解決策としては, setChecked() の前にリスナを消すとなっています.

//行の値設定
holder.sw_alarm.setOnCheckedChangeListener(null); //追加
holder.sw_alarm.setChecked(AlarmList.get(position).m_sw_alarm);
 :

ついでですが,

  if(holder.sw_alarm.isChecked()) {
    //mSwitch : Off -> On の時の処理
    // コンテキスト取得
    context = buttonView.getContext();
    //アラームスイッチ更新
    alarm_switch_upd(holder.sw_alarm.isChecked() ,position);
  } else {
    //mSwitch : On -> Off の時の処理
    // コンテキスト取得
    context = buttonView.getContext();
    //アラームスイッチ更新
    alarm_switch_upd(holder.sw_alarm.isChecked() ,position);
  }


この if 文は Off->On も On->Off も同じなのですが, if 文の必要があるのでしょうか.
また holder.sw_alarm.isChecked() はパラメータに isClicked がありますし, context は何に使うのか分かりません.

単純に

//アラームスイッチ更新
alarm_switch_upd(isChecked, position);


で済むように思います.


データベースの更新を MainActivity に移す例(カスタムリスナ)です:

AlarmsAdapter にリスナ関係を追加します.

  static interface AlarmSwitchListener {
    void onAlermSwitchChanged(int id, boolean isChecked);
  }
  private AlarmSwitchListener alarmSwitchListener;
  void setAlarmSwitchListener(AlarmSwitchListener l) {
    alarmSwitchListener = l;
  }


alarm_switch_upd の呼び出しをリスナの呼び出しに変更します.
その際, alarm_switch_upd 内にある sw_alarm の設定はこちらに抜き出しておきます.

    //アラームスイッチ変更
    holder.sw_alarm.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
      @Override
      public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
        AlarmList.get(position).sw_alarm = isChecked;
        if(alarmSwitchListener != null) { //リスナが登録されていたら呼び出す
          alarmSwitchListener.onAlermSwitchChanged(rowData.id, isChecked);
        }
      }
    });


MainActivity でリスナを実装, Adapter 作成時に自身をリスナとして登録します.

public class MainActivity extends AppCompatActivity implements AlarmsAdapter.AlarmSwitchListener {
  :
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    :
    AlarmsAdapter adapter = new AlarmsAdapter( ... );
    adapter.setAlarmSwitchListener(this);
    :
  }
  :
  //アラームスイッチ更新
  @Override
  public void onAlermSwitchChanged(int id, boolean isChecked) {
    //alarm_switch_upd にあった内容です.
    //dbオープン
    MySQLiteOpenHelper helper = new MySQLiteOpenHelper(this); //MainActivity は Context としても使えます
    try{
      mydb = helper.getWritableDatabase();
    }catch(SQLiteException e){
      //異常終了
      return;
    }

    //アラームテーブルのスイッチ更新
    String update_table = "update alarm_table " +
            "set switch = '" + isChecked + "'" +
            " where _id = " + id;
    mydb.execSQL(update_table);
  }

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/08/21 23:45

    動作はすると思いますが, これは設計の問題です.
    Adapter は通常 View のためのデータを保持するモノで, 画面操作の結果をデータベースに保存する機能というのは, Adapter の役割上からは越権行為と見られます.
    スイッチの操作によってデータベースを更新するのでしたら, Adapter に「スイッチが操作された場合に呼ばれるリスナインターフェース」を作り, それを MainActivity 等に implements させて Adapter に登録, スイッチが操作されたらそのリスナメソッドが実行され, context を得てデータベースを更新する... という形にしますと, Adapter の役割がはっきりします.

    キャンセル

  • 2019/08/22 00:04

    越権行為というのは、なるほどですね。
    RecyclerViewの動きが全てAdapter内に収まっているため、スイッチ変更時のデータベース更新のみリスナーインターフェースでMainActivityに移ると自分的にはソースが追いづらくなるイメージなんですが。。。悩みます。。

    キャンセル

  • 2019/08/22 00:09

    そうですね, 多数の画面がそれぞれデータベースを更新するような場合に問題となり易いということですので, 精々数箇所しか無い程度でどこまでやるかということはあると思います.

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 87.35%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る