RecyclerView の notifyItemChanged をもっと便利に使う

この記事は [Android その2 Advent Calendar 2017](https://qiita.com/advent-calendar/2017/android_second) の7日目の記事です。


---

## はじめに

RecyclerViewの登場から結構な日々が流れ、今やアプリで必須級のViewになっているのではないでしょうか。
RecyclerViewで表示されているデータに対して、追加や更新があった際にはAdapterクラスの`notifyItem`系メソッドを呼び出すのがほとんどだと思います。
しかし、「いいねの状態だけが変わった」ような時にそのアイテムのすべてのViewの更新を行ってしまうと、画像の読み込みがあったりする時にちらついてしまったりとつらい部分もあったりします。
そのような時に便利に使えるのが、`notifyItemChanged`メソッドと`notifyRangeItemChanged`メソッドのオーバーロードで存在している`payload`引数です。
この`payload`に渡したオブジェクトがAdapter側の`onBindViewHolder`で取得できるので、モデルの一部分に応じた変更だけ書けるというのが可能になります。

また、ちょうど今年のアドカレ期間中に似たテーマの記事が上がっていましたが、`DiffUtil`を使用するものとは違い、「何番目の要素がどう変わる」というのに特化しているのがこの記事の内容になります。

> DiffUtilを使用した記事はこちらです
[RecyclerViewの更新方法を考え直す](https://qiita.com/nakker1218/items/271760a2cfa90e41fb4e)

## 想定ケース

Twitterの「いいね」のようにRecyclerViewでリスト状になったアイテムに対して
ユーザーからの入力を受け付け、その操作でリクエストを送信し、押されたボタンの見た目を変化させます。
アイコンの画像の変更はボタンを押された直後に行い、リクエストの送信が失敗した時のみもともとの画像に戻す処理を行います。
いいねボタンに関しても、いい感じのアニメーションがかかるようにしておきます。

![device-2017-12-06-012625.png](https://qiita-image-store.s3.amazonaws.com/0/29459/ecc1fb60-42b2-5cd0-af77-1a9ade978d13.png)

> こちらの記事のViewをカスタマイズして使用しています
http://frogermcs.github.io/twitters-like-animation-in-android-alternative/

## リポジトリ

こちらにソースコードを上げています。
KotlinとRxをふんだんに使用しています。

https://github.com/r-ralph/RecyclerViewSample

## コード

Adapterを作成する際、通常だと`onBindViewHolder(ViewHolder holder, int position)`だけを実装し、その中でViewHolderに対して画像の設定やテキストの設定をしていると思います。

```SampleAdapter.kt
override fun onBindViewHolder(holder: SampleDataViewHolder, position: Int) {
    val item = data[position]
    holder.textView.text = item.name
    holder.likeButton.setState(item.isFavorite, false)
    holder.likeButton.setOnClickListener { subject.onNext(position) }
}
```

しかし、通常の`onBindViewHolder`だけだと、

 - `notifyItemChanged`による既にあるViewの一部分だけの変更
 - InflateされたViewへの最初のBind

のどちらでの呼び出しなのかわからなくなり、いいねボタンへのアニメーションの処理を書きにくくなります。

そこで、新しく`onBindViewHolder(ViewHolder holder, int position, List<Object> payloads)`の引数を取るオーバーロードメソッドを新たに実装します。

```SampleAdapter.kt
override fun onBindViewHolder(holder: SampleDataViewHolder, position: Int, payloads: MutableList<Any>) {
    if (payloads.any()) {
        val payload = payloads[0] as? Pair<*, *>
        when (payload?.first as? String) {
            PAYLOAD_UPDATE_STATE -> {
                (payload.second as? Boolean)?.let {
                    holder.likeButton.setState(it, false)
                }
            }
        }
    }
    onBindViewHolder(holder, position)
}

companion object {
    const val PAYLOAD_UPDATE_STATE = "update_state"
}
```

ここで重要なのは、payloadが与えられない場合は通常の`onBindViewHolder`を行なわなければ行けないという部分です。（呼び出さずに続けて書いてもいいですが、payload付きの`onBindViewHolder`は一部更新、付いていない方はすべて更新としておいたほうが役割的にもわかりやすいです。）

`payload`として受け取るものはなんでもいい（Object）ので、今回はKotlinのPairクラスで、`何を更新するか,何の値に更新するか`をペアに持つオブジェクトを受け取るようにしています。

次に、Activity・Fragment側です。

```MainActivity.java
private fun changeLikeState(position: Int) {
    val model = data[position]
    val previousState = model.isFavorite
    model.isFavorite = !model.isFavorite
    val id = model.id
    Log.d("MainActivity", "Start (id: $id, newState: ${model.isFavorite})")
    adapter?.notifyItemChanged(position, SampleAdapter.PAYLOAD_UPDATE_STATE to model.isFavorite)
    if (!previousState) {
        ApiClient.like(id)
                .bindToLifecycle(this)
                .subscribeOnIoThread()
                .observeOnMainThread()
                .subscribe({
                    Log.d("MainActivity", "Success like (id: $id, newState: ${model.isFavorite})")
                }, {
                    Log.d("MainActivity", "Error like(id: $id, newState: ${model.isFavorite})")
                    model.isFavorite = false
                    adapter?.notifyItemChanged(position, SampleAdapter.PAYLOAD_UPDATE_STATE to false)
                })
    } else {
        ApiClient.unlike(id)
                .bindToLifecycle(this)
                .subscribeOnIoThread()
                .observeOnMainThread()
                .subscribe({
                    Log.d("MainActivity", "Success unlike (id: $id, newState: ${model.isFavorite})")
                }, {
                    Log.d("MainActivity", "Error unlike (id: $id, newState: ${model.isFavorite})")
                    model.isFavorite = true
                    adapter?.notifyItemChanged(position, SampleAdapter.PAYLOAD_UPDATE_STATE to true)
                })
    }
}
```

重要なのは、`adapter?.notifyItemChanged`を呼び出している部分で、`SampleAdapter.PAYLOAD_UPDATE_STATE to model.isFavorite`とすることで先程の要件を満たすPairオブジェクトを作成しています。
呼び出すタイミングは、APIリクエストを投げる前と、投げて失敗した時になります。

サンプルプロジェクトでは、奇数番目と偶数番目の要素でいいねが成功・失敗が切り替わるようになっています。

## まとめ

`notifyItemChanged`メソッドの`payload`を指定することで、より細かくRecyclerViewのViewを更新させることが可能になります。
パフォーマンス的にも、不要な部分の更新は行わないようにすることで、向上が見込めます。