RecyclerViewはアイテムを一覧表示してくれます。
ただ一覧表示するだけではなく、「追加・削除・移動・切り替え」といったアイテムの表示を効率よく変更する仕組み持っています。
今回はこの仕組みを使ったアイテムの変更方法を紹介します。
アイテムの変更手順
アイテムの変更を行う手順は次の通りです。
- (1)データの変更
- (2)変更の通知(表示を更新)
(1)データの変更
アイテムデータはアダプターが参照(Kotlinの引数は参照渡し)しています。
参照元のデータを変更すれば、変更内容がそのままアダプターに伝わります。
ただし、全てのアイテムデータを入れ替えるために、参照先を変更する場合は注意してください。
例えばデータがCursorなどの場合、旧参照先の解放(Cursor#close)を忘れてはいけません。
(2)変更の通知(表示を更新)
データの変更が終わったら、アダプターへ変更の通知(notifyメソッドの実行)を行います。
変更の通知を受け取ったアダプターは、アイテムの表示を更新するようにRecyclerViewへ要求を出します。
変更箇所のみを更新するように、変更内容に合ったnotifyメソッドが用意されています。
notifyメソッド | 変更箇所 | observerメソッド | |
---|---|---|---|
更新 | notifyDataSetChanged( ) | 全データ | onChanged |
変更 | notifyItemChanged(position: Int) | position | onItemRangeChanged |
notifyItemRangeChanged( positionStart: Int, itemCount: Int) | positionStartから itemCount個 |
||
挿入 | notifyItemInserted(position: Int) | position | onItemRangeInserted |
notifyItemRangeInserted( positionStart: Int, itemCount: Int) | positionStartから itemCount個 |
||
削除 | notifyItemRemoved(position: Int) | position | onItemRangeRemoved |
notifyItemRangeRemoved( positionStart: Int, itemCount: Int) | positionStartから itemCount個 |
||
移動 from⇒to | notifyItemMoved( fromPosition: Int, toPosition: Int) | fromPosition toPosition | onItemRangeMoved |
変更 payload付 | notifyItemChanged( position: Int, payload: Any?) | position | onItemRangeChanged |
notifyItemRangeChanged( positionStart: Int, itemCount: Int, payload: Any?) | positionStartから itemCount個 |
常に全てのアイテムの表示を更新させることも可能ですが、変更のない箇所を更新することは無駄です。
端末のリソース(CPUの処理能力、バッテリー、など)消費の低減やパフォーマンスを確保するために、notifyメソッドは使い分けるようにしましょう。
アイテムの変更例
アイテムの変更手順を先に示しました。
アイテムデータがListで構成されたアダプターを題材にして、アイテムの変更を行う例をあげます。
全データを更新(notifyDataSetChanged)
アイテムのデータの参照先を変更して、全データを更新しています。
... val _items = mutableListOf( "Item 0", "Item 1", "Item 2", "Item 3", "Item 4", "Item 5", "Item 6", "Item 7", "Item 8", "Item 9", "Item A", "Item B", "Item C", "Item D", "Item E", "Item F" ) change(_items) ... } private fun change(items: MutableList<String>) { val _adapter = rcySample.adapter as ListAdapter _adapter.items = items // データの変更 _adapter.notifyDataSetChanged() // 変更の通知(表示を更新) }
アイテムを変更(notifyItemChanged)
ポジション3のアイテムを変更しています。
... change(3, "Item X(Change)") ... } private fun change(position: Int, item: String) { val _adapter = rcySample.adapter as ListAdapter _adapter.items[position] = item // データの変更 _adapter.notifyItemChanged(position) // 変更の通知(表示を更新) }
指定範囲を変更(notifyItemRangeChanged)
ポジション3の位置から3つのアイテムを変更しています。
... val _items = arrayOf("Item X(Change)", "Item Y(Change)", "Item Z(Change)") change(3, _items) ... } private fun change(position: Int, items: Array<String>) { val _adapter = rcySample.adapter as ListAdapter for(i in 0 until items.size) // データの変更 _adapter.items[position + i] = items[i] _adapter.notifyItemRangeChanged(position, items.size) // 変更の通知(表示を更新) }
アイテムを挿入(notifyItemInserted)
ポジション3へアイテムを挿入しています。
... insert(3, "Item X(Insert)") ... } private fun insert(position: Int, item: String) { val _adapter = rcySample.adapter as ListAdapter _adapter.items.add(position, item) // データの変更 _adapter.notifyItemInserted(position) // 変更の通知(表示を更新) }
指定個数を挿入(notifyItemRangeInserted)
ポジション3へ3つのアイテムを挿入しています。
... val _items = arrayOf("Item X(Insert)", "Item Y(Insert)", "Item Z(Insert)") insert(3, _items) ... } private fun insert(position: Int, items: Array<String>) { val _adapter = rcySample.adapter as ListAdapter for(i in 0 until items.size) // データの変更 _adapter.items.add(position + i, items[i]) _adapter.notifyItemRangeInserted(position, items.size) // 変更の通知(表示を更新) }
アイテムを削除(notifyItemRemoved)
ポジション3のアイテムを削除しています。
... remove(3) ... } private fun remove(position: Int) { val _adapter = rcySample.adapter as ListAdapter _adapter.items.removeAt(position) // データの変更 _adapter.notifyItemRemoved(position) // 変更の通知(表示を更新) }
指定範囲を削除(notifyItemRangeRemoved)
ポジション3の位置から3つのアイテムを削除しています。
... remove(3, 3) ... } private fun remove(position: Int, count: Int) { val _adapter = rcySample.adapter as ListAdapter for(i in 0 until count) // データの変更 _adapter.items.removeAt(position) _adapter.notifyItemRangeRemoved(position, count) // 変更の通知(表示を更新) }
アイテムを移動(notifyItemMoved)
ポジション2のアイテムをポジション5へ移動しています。
... move(2, 5) ... } private fun move(from: Int, to: Int) { val _adapter = rcySample.adapter as ListAdapter val _item = _adapter.items[from] // データの変更 _adapter.items.removeAt(from) _adapter.items.add(to, _item) _adapter.notifyItemMoved(from, to) // 変更の通知(表示を更新) }
「変更の通知(表示を更新)」の裏側
notifyItemChange( )を例に、notifyメソッドの動作を追ってみましょう。
AdapterDataObserverのメソッドが実行される
追っていくと、AdapterDataObservable#mObserversから要素を取り出し(get(i)の部分)、要素が持つメソッド(onItemRangeChanged)を実行していることが分かります。
※***Observableと***Observersの2つが登場します。名前が似ているので間違えないように!
public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild2, NestedScrollingChild3 { ... // 6984行近辺 public abstract static class Adapter<VH extends ViewHolder> { private final AdapterDataObservable mObservable = new AdapterDataObservable(); ... // 7369行近辺 public final void notifyItemChanged(int position) { mObservable.notifyItemRangeChanged(position, 1); } ... } ... // 12242行近辺 static class AdapterDataObservable extends Observable<AdapterDataObserver> { public boolean hasObservers() { return !mObservers.isEmpty(); } ... public void notifyItemRangeChanged(int positionStart, int itemCount) { notifyItemRangeChanged(positionStart, itemCount, null); } public void notifyItemRangeChanged(int positionStart, int itemCount, @Nullable Object payload) { ... for (int i = mObservers.size() - 1; i >= 0; i--) { mObservers.get(i).onItemRangeChanged(positionStart, itemCount, payload); } } ... } ... }
mObserversとは、AdapterDataObservableのSuperクラス:Observableが持つフィールドです。
単なるListでAdapterDataObserver型を格納(ジェネリクス:Tで指定)します。
public abstract class Observable<T> { ... protected final ArrayList<T> mObservers = new ArrayList<T>(); ... public void registerObserver(T observer) { ... synchronized(mObservers) { ... mObservers.add(observer); } } ... public void unregisterObserver(T observer) { ... synchronized(mObservers) { ... mObservers.remove(index); } }
つまり、AdapterDataObserverのメソッドonItemRangeChanged( )が実行されます。
デフォルトのRecyclerViewDataObserver
アダプターのインスタンスを作成した時点は、mObserversの要素はありません。
要素がないのでnotifyメソッドを実行しても何も起こらないはずですが、アイテムの更新が行われます。
これは、RecyclerViewへアダプターを設定する時に、RecyclerViewDataObserverが登録されるからです。
つまり、mObserversはデフォルトでRecyclerViewDataObserverを持ち、notifyメソッドの実行でRecyclerViewDataObserver#onItemRangeChanged( )が実行されます。
public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild2, NestedScrollingChild3 { ... // 365行近辺 private final RecyclerViewDataObserver mObserver = new RecyclerViewDataObserver(); ... // 1158行近辺 public void setAdapter(@Nullable Adapter adapter) { // bail out if layout is frozen setLayoutFrozen(false); setAdapterInternal(adapter, false, true); processDataSetCompletelyChanged(false); requestLayout(); } ... // 1195行近辺 private void setAdapterInternal(@Nullable Adapter adapter, boolean compatibleWithPrevious, boolean removeAndRecycleViews) { ... mAdapter = adapter; if (adapter != null) { adapter.registerAdapterDataObserver(mObserver); adapter.onAttachedToRecyclerView(this); } ... } ... public abstract static class Adapter<VH extends ViewHolder> { private final AdapterDataObservable mObservable = new AdapterDataObservable(); ... // 7286行近辺 public void registerAdapterDataObserver(@NonNull AdapterDataObserver observer) { mObservable.registerObserver(observer); } ... // 7300行近辺 public void unregisterAdapterDataObserver(@NonNull AdapterDataObserver observer) { mObservable.unregisterObserver(observer); } ... } }
RecyclerViewDataObserverの実装
RecyclerViewDataObserverは、変更内容に合ったアイテムの描画コマンドをAdapterHelperクラスへ設定した後に、View#requestLaytout( )を発行してRecyclerViewの再描画を依頼するのが仕事です。
RecyclerViewはこの描画コマンドを見てアイテムの描画を行います。
public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild2, NestedScrollingChild3 { ... // 398行近辺 final Runnable mUpdateChildViewsRunnable = new Runnable() { @Override public void run() { if (!mFirstLayoutComplete || isLayoutRequested()) { // a layout request will happen, we should not do layout here. return; } if (!mIsAttached) { requestLayout(); // if we are not attached yet, mark us as requiring layout and skip return; } if (mLayoutSuppressed) { mLayoutWasDefered = true; return; //we'll process updates when ice age ends. } consumePendingUpdateOperations(); } }; ... // 5530行近辺 private class RecyclerViewDataObserver extends AdapterDataObserver { ... @Override public void onItemRangeChanged(int positionStart, int itemCount, Object payload) { assertNotInLayoutOrScroll(null); if (mAdapterHelper.onItemRangeChanged(positionStart, itemCount, payload)) { triggerUpdateProcessor(); //←requestLayout( )発行↑描画コマンド設定 } } ... void triggerUpdateProcessor() { if (POST_UPDATES_ON_ANIMATION && mHasFixedSize && mIsAttached) { ViewCompat.postOnAnimation(RecyclerView.this, mUpdateChildViewsRunnable); } else { mAdapterUpdateDuringMeasure = true; requestLayout(); } } } ... }
Payload付きnotifyメソッド
notifyItemChangedとnotifyItemRangeChangedは引数にpayloadを持つものが用意されています。
このメソッドが呼び出された場合、Adapter#onBindViewHolder( )の引数へpayloadsが渡されます。
アイテムの変更例
アイテムを変更(notifyItemChanged)
ポジション3のアイテムを変更しています。
... change(3, "Item X(Change)") ... } private fun change(position: Int, item: String) { val _adapter = rcySample.adapter as ListAdapter _adapter.notifyItemChanged(position, item) // 変更の通知(表示を更新) }
class ListAdapter(var items: MutableList<String>) : RecyclerView.Adapter<ListAdapter.ListViewHolder>() { ... override fun onBindViewHolder(holder: ListViewHolder, position: Int) { holder.mesg.text = items[position] // 要素とデータを紐づける } override fun onBindViewHolder( holder: ListViewHolder, position: Int, payloads: MutableList<Any>) { payloads?.let { if(it.size > 0) items[position] = it[0] as String } super.onBindViewHolder(holder, position, payloads) } ... }
指定範囲を変更(notifyItemRangeChanged)
ポジション3の位置から3つのアイテムを変更しています。
... change(3, 3, "Item Y(Change)") ... } private fun change(position: Int, count: Int, item: String) { val _adapter = rcySample.adapter as ListAdapter _adapter.notifyItemRangeChanged(position, count, item) // 変更の通知(表示を更新) }
class ListAdapter(var items: MutableList<String>) : RecyclerView.Adapter<ListAdapter.ListViewHolder>() { ... override fun onBindViewHolder(holder: ListViewHolder, position: Int) { holder.mesg.text = items[position] // 要素とデータを紐づける } override fun onBindViewHolder( holder: ListViewHolder, position: Int, payloads: MutableList<Any>) { payloads?.let { if(it.size > 0) items[position] = it[0] as String } super.onBindViewHolder(holder, position, payloads) } ... }
※payloadsはListですが、複数のデータをListに含めて渡す方法が不明です。
注意点
onBindViewHolder( )は(1)payloads有りと(2)payloads無しの2つメソッドがあります。
public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild2, NestedScrollingChild3 { ... public abstract static class Adapter<VH extends ViewHolder> { ... // 7032行近辺 ↓(2)payloads無し public abstract void onBindViewHolder(@NonNull VH holder, int position); ... // 7063行近辺 ↓(1)payloads有り public void onBindViewHolder(@NonNull VH holder, int position, @NonNull List<Object> payloads) { onBindViewHolder(holder, position); // (2)payloads無しが呼ばれる } ... } ... }
payloadsを使用するカスタムAdapterを作成する時、(1)payloads有りのonBindViewHolderをオーバーライドすればよいのですが、(2)payloads無しのonBindViewHolderはabstractなので必ず実装が必要です。
また、onBaindViewHolder( )が実行される時、外部からは(1)payloads有りのonBindViewHolderが呼ばれます。(2)payloads無しのonBindViewHolderは(1)payloads有り側から呼ばれる下位メソッドになっています。
関連記事: