RecyclerView#onBindViewHolderでイベントリスナを登録するときの注意

RecyclerView#onBindViewHolderViewHolder内の要素に対してイベントリスナを登録するときは注意が必要。

RecyclerViewViewHolderがいくつあっても画面に見えている部分だけインスタンス化されている。
スクロールによって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 { ... }
    }
  }
}