16
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

RecyclerViewでnotifyItemInsertedやnotifyItemRemovedを使う時の注意点

Last updated at Posted at 2019-06-27

背景

notifyItemInsertednotifyItemRemovedを使っていると、消したいデータと違うデータが消えたり、IndexOutOfExceptionが起こってアプリがクラッシュしたので、原因を調べてみました。

以下の3つを順番に実行した時のAdapter内のリストの状態RecyclerViewのデータ項目の状態アプリ画面の状態がどうなっているのか図で示しながら解説していきます。その後、対処法を示していきます。

  • RecyclerViewを初期化した時
  • リストにデータを追加した時
  • notifyItemInsertedを呼び出した時

RecyclerViewを初期化した時

今回、原因を調べるために作ったAdapterクラスです。それぞれの状態だけを見ると、間違っていなさそうに見えます。
しかし、この状態で削除ボタン(R.id.deleteButton)を押すと、一度目は正しいデータが削除されますが、二度目は、違うデータが消えたり、IndexOutOfExceptionが起こります。

MyRecyclerViewAdapter.kt
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)
    }
}
スクリーンショット 2019-06-27 10.59.14.png

リストにデータを追加した時

Adapter内のリストには変化がありますが、データを追加したことを通知していないため、RecyclerViewのデータ項目の状態やアプリ画面に変化はありません。

MainActivity.kt
//追加するコード
addButton.setOnClickListener {
    (recyclerView.adapter as MyRecyclerViewAdapter).addItem(1, "Z")
}
スクリーンショット 2019-06-27 10.59.24.png

notifyItemInserted(position=1)を実行した時

notifyItemInserted(position: Int)は、指定した位置に新しいデータが追加されたことを通知しています。指定した位置にあったデータ項目は元の位置 + 1されます。追加したデータは指定した位置でバインドされます。しかし、位置がズレたデータ項目は、リバインドされないのでTextViewや削除ボタンの処理が更新されません。そのため位置2にある削除ボタンの処理を実行すると、リスト.removeAt(1)が実行され、消したいデータと違ったデータが削除されるわけです。

MainActivity.kt
addButton.setOnClickListener {
    (recyclerView.adapter as MyRecyclerViewAdapter).addItem(1, "Z")    
    //追加するコード
    recyclerView.adapter?.notifyItemInserted(1)
}
スクリーンショット 2019-06-27 10.59.32.png

対処方法

追加や削除を通知した後、notifyItemRangeChangedを使う

notifyItemRangeChanged(startPosition: Int, itemCount: Int)は、startPositionからitemCountまでの範囲に、データの変更があったことを通知し、リバインドしてくれます。そのため、TextViewの値や削除ボタンの処理が適したものに更新されます。

MainActivity.kt
addButton.setOnClickListener {
    (recyclerView.adapter as MyRecyclerViewAdapter).addItem(1, "Z")    
    recyclerView.adapter?.notifyItemInserted(1)
    //追加するコード
    recyclerView.adapter?.notifyItemRangeChanged(2, sampleList.size)
}
MyRecyclerViewAdapter.kt

override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
    holder.contentTextView.text = "${sampleList[position]} + $position"
    holder.deleteButton.setOnClickListener {
        sampleList.removeAt(position)
        notifyItemRemoved(position)
        //追加するコード
        notifyItemRangeChanged(position, itemCount)
    }
}
スクリーンショット 2019-06-27 13.44.22.png

まとめ

notifyItemInsertednotifyItemRemovedを使うなら、notifyItemRangeChangedを一緒に使いましょう。
notifyDataSetChangedを使うとどちらもやってくれますが、Documentであまりおすすめされていないのとアニメーションがされません。
もし、ここ間違っているよ等あれば、コメントで教えてください!

16
14
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?