RecyclerView#onBindViewHolderでイベントリスナを登録するときの注意
RecyclerView#onBindViewHolder
でViewHolder
内の要素に対してイベントリスナを登録するときは注意が必要。
RecyclerView
はViewHolder
がいくつあっても画面に見えている部分だけインスタンス化されている。
スクロールによってViewHolder
が隠れるのと同時に新しいViewHolder
が表示され、そのとき隠れたViewHolder
が使い回される。
隠れたViewHolder
が使い回されるとイベントリスナは消えてないので新たに表示されるViewHolder
でも残っているので、
あるViewHolder
に対して行なった操作が、別のViewHolder
に対する処理を行なうことになる。
そのため、onBindViewHolder
でイベントリスナを登録するような場合は使い回されることを考慮して、
既存のイベントリスナがあれば破棄したうえで再度登録する必要がある。
イベントリスナを上書きする場合
setOnClickListener
など、set
が接頭語についているタイプのメソッドは上書きになるので上記問題は起こらない。
ただし、条件によってセットしたりしなかったりする場合は削除して上げる必要がある。
例えば以下のようなコードを考える。
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
FooViewHolder -> {
// 条件によってはsetOnClickListenerが呼ばれないことがある。
if (条件) {
holder.textView.setOnClickListener { ... }
}
}
}
}
この例では条件
がfalse
のときはsetOnClickListener
が呼ばれず、もし使い回される前はsetOnClickListener
が呼ばれていた場合、そのListenerが残っていることになる。
するとこのViewHolder
をクリックしたときに行なわれる処理は使い回される前のViewHolder
に対して行なわれることになる。
これを避けるには以下のように一度リセットしておけばよい。
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
FooViewHolder -> {
// 一度リスナを消しておく
holder.textView.setOnClickListener(null)
// 条件によってはsetOnClickListenerが呼ばれないことがある。
if (条件) {
holder.textView.setOnClickListener { ... }
}
}
}
}
イベントリスナを追加する場合
doOnTextChanged
メソッドなど、イベントリスナを追加するタイプの場合は、一度すべてのリスナを削除して登録しなおすなどの対処が必要となる。
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
FooViewHolder -> {
binding.username.removeCallbacks {}
holder.textView.doOnTextChanged { ... }
}
}
}