【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
}
}
チェックボックスにチェックを付けた後、
新規登録しようとすると、
下のようなエラーメッセージが出て落ちた。
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で似たような事例を見つけた。
どうも、
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行足せばいいから楽といえば楽。