Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

この記事は Android その2 Advent Calendar 2017 の7日目の記事です。


はじめに

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

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

DiffUtilを使用した記事はこちらです
RecyclerViewの更新方法を考え直す

想定ケース

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

device-2017-12-06-012625.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を更新させることが可能になります。
パフォーマンス的にも、不要な部分の更新は行わないようにすることで、向上が見込めます。

ralph
Android App Developer / Android / Minecraft / Xamarin / Java / Kotlin / C#
https://www.ralph.ms/
mspjp
Microsoft製品の楽しさを学生に伝えるために活動する学生団体
http://mspjp.net
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away