【Android Studio】Cannot call this method while RecyclerView is computing a layout or scrollingが出たときの対処法

Realmに保存したデータをRecyclerViewで表示して、チェックが付いたらisCheckedをtrueにしようとしたところこのエラーが出たのでその備忘録。

 

 

ソースコードの問題箇所

RealmのデータをRecyclerViewに表示するためのAdapterを下のように実装したのだが、

class TaskAdapterMain(val context: Context, data: OrderedRealmCollection) :
    RealmRecyclerViewAdapter<Task, TaskAdapterMain.ViewHolder>(data, true){
/* 中略 */
class ViewHolder(cell: View) : RecyclerView.ViewHolder(cell) { val task: CheckBox = cell.findViewById(R.id.taskCheck) //タスクのためのtextview } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val inflater = LayoutInflater.from(parent.context) val view = inflater.inflate(R.layout.unfinished_task_layout, parent, false) return ViewHolder(view) } override fun onBindViewHolder(holder: TaskAdapterMain.ViewHolder, position: Int) { val task: Task? = getItem(position) /* 中略 */ task?.isFinished?.let { holder.task.isChecked = it //isFinishedをCheckBoxの状態に反映 }

/* 中略 */
holder.itemView.taskCheck.setOnCheckedChangeListener{ view, isChecked -> this.checkBoxListener?.invoke(task?.id, isChecked) notifyDataSetChanged() } } override fun getItemId(position: Int): Long { return getItem(position)?.id ?: 0 } }


 チェックボックスにチェックを付けた後、

f:id:MiCan:20200902112744j:plain

 

新規登録しようとすると、

f:id:MiCan:20200902112753j:plain

 

下のようなエラーメッセージが出て落ちた。

JNI DETECTED ERROR IN APPLICATION: JNI NewLocalRef called with pending exception java.lang.IllegalStateException: Cannot call this method while RecyclerView is computing a layout or scrolling androidx.recyclerview.widget.RecyclerView{5739a9b VFED..... ......ID 0,0-1080,729 #7f0800ee app:id/taskList}, adapter:com.example.taskreminder.TaskAdapterMain@2269c66, layout:androidx.recyclerview.widget.LinearLayoutManager@afbdba7, context:com.example.taskreminder.MainActivity@338c263

何が原因なのかと調べていたところStackOverflowで似たような事例を見つけた。

stackoverflow.com

 どうも、

onBindViewHolder()

→ holder.task.isCheckedが変更される

→ setOnCheckedChangeListenerが呼ばれる

→ notifyDatasetChanged()

→ onBindViewHolder()

→ ...

という無限ループが生じるのが原因らしい。対処法もいくつか挙げられていたがその中から2つ。

 

対処法1. Boolean型変数onBindを導入する

単純に、onBindViewHolder内でisCheckedが変更された時にはsetOnCheckedChangeListenerを呼ばないようにすればいいんじゃね?って話。コードを次のように変更してみる。

class TaskAdapterMain(val context: Context, data: OrderedRealmCollection) :
    RealmRecyclerViewAdapter<Task, TaskAdapterMain.ViewHolder>(data, true){
onBind: Boolean = false /* 中略 */
class ViewHolder(cell: View) : RecyclerView.ViewHolder(cell) { val task: CheckBox = cell.findViewById(R.id.taskCheck) //タスクのためのtextview } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val inflater = LayoutInflater.from(parent.context) val view = inflater.inflate(R.layout.unfinished_task_layout, parent, false) return ViewHolder(view) } override fun onBindViewHolder(holder: TaskAdapterMain.ViewHolder, position: Int) { val task: Task? = getItem(position)
/* 中略 */
task?.isFinished?.let { onBind = true holder.task.isChecked = it //isFinishedをCheckBoxの状態に反映 onBind = false }

/* 中略 */
holder.itemView.taskCheck.setOnCheckedChangeListener{ view, isChecked -> if (!onBind) { this.checkBoxListener?.invoke(task?.id, isChecked) notifyDataSetChanged() } } } override fun getItemId(position: Int): Long { return getItem(position)?.id ?: 0 } }

こうすることで、onBindViewHolderからsetOnCheckedChangeListenerが呼ばれた時にはonBindがtrueであるため、notifyDateSetChangedが呼ばれずに済む。めでたしめでたし。一番ベタな方法かも。

 

対処法2. setOnCheckedChangeListenerを再定義する

isCheckedを変更する前と後で2回Listenerを定義することでもエラーを回避できる。

 

class TaskAdapterMain(val context: Context, data: OrderedRealmCollection) :
    RealmRecyclerViewAdapter<Task, TaskAdapterMain.ViewHolder>(data, true){
/* 中略 */
override fun onBindViewHolder(holder: TaskAdapterMain.ViewHolder, position: Int) { val task: Task? = getItem(position) /* 中略 */     holder.itemView.taskCheck.setOnCheckedChangeListener{ _,_ -> null } task?.isFinished?.let { holder.task.isChecked = it //isFinishedをCheckBoxの状態に反映 }

/* 中略 */
holder.itemView.taskCheck.setOnCheckedChangeListener{ view, isChecked -> this.checkBoxListener?.invoke(task?.id, isChecked) notifyDataSetChanged() } } override fun getItemId(position: Int): Long { return getItem(position)?.id ?: 0 } }

こうすれば、isCheckedが変更された時には直前に書かれた方の定義が利用される(nullを返すのみ)から上手くいくというわけか。なるほど。1行足せばいいから楽といえば楽。