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

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

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

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

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

```kotlin: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)
    }
}
```

<img width="1197" alt="スクリーンショット 2019-06-27 10.59.14.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/434603/8d9c3ac6-d4d2-4e2d-cdc2-de97be05653d.png">

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

```kotlin:MainActivity.kt
//追加するコード
addButton.setOnClickListener {
    (recyclerView.adapter as MyRecyclerViewAdapter).addItem(1, "Z")
}
```

<img width="1197" alt="スクリーンショット 2019-06-27 10.59.24.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/434603/a1612cd4-69d0-55b7-328d-ed2ac82f6bc1.png">

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

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


<img width="1197" alt="スクリーンショット 2019-06-27 10.59.32.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/434603/21738757-e3e8-8fa2-f28f-5295560378ee.png">

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

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

```kotlin: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)
    }
}
```

<img width="1419" alt="スクリーンショット 2019-06-27 13.44.22.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/434603/3db802ab-2075-aa28-e189-7787a5df34e7.png">


# まとめ
`notifyItemInserted`や`notifyItemRemoved`を使うなら、`notifyItemRangeChanged`を一緒に使いましょう。
`notifyDataSetChanged`を使うとどちらもやってくれますが、[Document](https://developer.android.com/reference/android/support/v7/widget/RecyclerView.Adapter.html#notifyDataSetChanged())であまりおすすめされていないのとアニメーションがされません。
もし、ここ間違っているよ等あれば、コメントで教えてください！