この記事は Android その2 Advent Calendar 2017 の7日目の記事です。
はじめに
RecyclerViewの登場から結構な日々が流れ、今やアプリで必須級のViewになっているのではないでしょうか。
RecyclerViewで表示されているデータに対して、追加や更新があった際にはAdapterクラスのnotifyItem
系メソッドを呼び出すのがほとんどだと思います。
しかし、「いいねの状態だけが変わった」ような時にそのアイテムのすべてのViewの更新を行ってしまうと、画像の読み込みがあったりする時にちらついてしまったりとつらい部分もあったりします。
そのような時に便利に使えるのが、notifyItemChanged
メソッドとnotifyRangeItemChanged
メソッドのオーバーロードで存在しているpayload
引数です。
このpayload
に渡したオブジェクトがAdapter側のonBindViewHolder
で取得できるので、モデルの一部分に応じた変更だけ書けるというのが可能になります。
また、ちょうど今年のアドカレ期間中に似たテーマの記事が上がっていましたが、DiffUtil
を使用するものとは違い、「何番目の要素がどう変わる」というのに特化しているのがこの記事の内容になります。
DiffUtilを使用した記事はこちらです
RecyclerViewの更新方法を考え直す
想定ケース
Twitterの「いいね」のようにRecyclerViewでリスト状になったアイテムに対して
ユーザーからの入力を受け付け、その操作でリクエストを送信し、押されたボタンの見た目を変化させます。
アイコンの画像の変更はボタンを押された直後に行い、リクエストの送信が失敗した時のみもともとの画像に戻す処理を行います。
いいねボタンに関しても、いい感じのアニメーションがかかるようにしておきます。
こちらの記事のViewをカスタマイズして使用しています
http://frogermcs.github.io/twitters-like-animation-in-android-alternative/
リポジトリ
こちらにソースコードを上げています。
KotlinとRxをふんだんに使用しています。
コード
Adapterを作成する際、通常だとonBindViewHolder(ViewHolder holder, int position)
だけを実装し、その中でViewHolderに対して画像の設定やテキストの設定をしていると思います。
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)
の引数を取るオーバーロードメソッドを新たに実装します。
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側です。
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を更新させることが可能になります。
パフォーマンス的にも、不要な部分の更新は行わないようにすることで、向上が見込めます。