背景
notifyItemInserted
やnotifyItemRemoved
を使っていると、消したいデータと違うデータが消えたり、IndexOutOfExceptionが起こってアプリがクラッシュしたので、原因を調べてみました。
以下の3つを順番に実行した時のAdapter内のリストの状態
・RecyclerViewのデータ項目の状態
・アプリ画面の状態
がどうなっているのか図で示しながら解説していきます。その後、対処法を示していきます。
- RecyclerViewを初期化した時
- リストにデータを追加した時
- notifyItemInsertedを呼び出した時
RecyclerViewを初期化した時
今回、原因を調べるために作ったAdapterクラスです。それぞれの状態だけを見ると、間違っていなさそうに見えます。
しかし、この状態で削除ボタン(R.id.deleteButton)を押すと、一度目は正しいデータが削除されますが、二度目は、違うデータが消えたり、IndexOutOfExceptionが起こります。
class MyRecyclerViewAdapter(private val context: Context, private val sampleList: MutableList<String>) :
RecyclerView.Adapter<MyRecyclerViewAdapter.MyViewHolder>() {
class MyViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val contentTextView: TextView = view.findViewById(R.id.content)
val deleteButton: Button = view.findViewById(R.id.deleteButton)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
return MyViewHolder(LayoutInflater.from(context).inflate(R.layout.sample_list, parent, false))
}
override fun getItemCount(): Int = sampleList.size
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
holder.contentTextView.text = "${sampleList[position]} + $position"
holder.deleteButton.setOnClickListener {
sampleList.removeAt(position)
notifyItemRemoved(position)
}
}
fun addItem(position: Int, string: String){
sampleList.add(position, string)
}
}
リストにデータを追加した時
Adapter内のリストには変化がありますが、データを追加したことを通知していないため、RecyclerViewのデータ項目の状態やアプリ画面に変化はありません。
//追加するコード
addButton.setOnClickListener {
(recyclerView.adapter as MyRecyclerViewAdapter).addItem(1, "Z")
}
notifyItemInserted(position=1)を実行した時
notifyItemInserted(position: Int)
は、指定した位置に新しいデータが追加されたことを通知しています。指定した位置にあったデータ項目は元の位置 + 1
されます。追加したデータは指定した位置でバインドされます。しかし、位置がズレたデータ項目は、リバインドされないのでTextViewや削除ボタンの処理が更新されません。そのため位置2にある削除ボタンの処理を実行すると、リスト.removeAt(1)
が実行され、消したいデータと違ったデータが削除されるわけです。
addButton.setOnClickListener {
(recyclerView.adapter as MyRecyclerViewAdapter).addItem(1, "Z")
//追加するコード
recyclerView.adapter?.notifyItemInserted(1)
}
対処方法
追加や削除を通知した後、notifyItemRangeChangedを使う
notifyItemRangeChanged(startPosition: Int, itemCount: Int)
は、startPosition
からitemCount
までの範囲に、データの変更があったことを通知し、リバインドしてくれます。そのため、TextViewの値や削除ボタンの処理が適したものに更新されます。
addButton.setOnClickListener {
(recyclerView.adapter as MyRecyclerViewAdapter).addItem(1, "Z")
recyclerView.adapter?.notifyItemInserted(1)
//追加するコード
recyclerView.adapter?.notifyItemRangeChanged(2, sampleList.size)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
holder.contentTextView.text = "${sampleList[position]} + $position"
holder.deleteButton.setOnClickListener {
sampleList.removeAt(position)
notifyItemRemoved(position)
//追加するコード
notifyItemRangeChanged(position, itemCount)
}
}
まとめ
notifyItemInserted
やnotifyItemRemoved
を使うなら、notifyItemRangeChanged
を一緒に使いましょう。
notifyDataSetChanged
を使うとどちらもやってくれますが、Documentであまりおすすめされていないのとアニメーションがされません。
もし、ここ間違っているよ等あれば、コメントで教えてください!