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)
アイテムのデータの参照先を変更して、全データを更新しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | ... 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のアイテムを変更しています。
1 2 3 4 5 6 7 8 9 10 | ... 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つのアイテムを変更しています。
1 2 3 4 5 6 7 8 9 10 11 12 | ... 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へアイテムを挿入しています。
1 2 3 4 5 6 7 8 9 10 | ... 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つのアイテムを挿入しています。
1 2 3 4 5 6 7 8 9 10 11 12 | ... 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のアイテムを削除しています。
1 2 3 4 5 6 7 8 9 10 | ... remove( 3 ) ... } private fun remove(position: Int) { val _adapter = rcySample.adapter as ListAdapter _adapter.items.removeAt(position) // データの変更 _adapter.notifyItemRemoved(position) // 変更の通知(表示を更新) } |
指定範囲を削除(notifyItemRangeRemoved)
ポジション3の位置から3つのアイテムを削除しています。
1 2 3 4 5 6 7 8 9 10 11 | ... 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へ移動しています。
1 2 3 4 5 6 7 8 9 10 11 12 | ... 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つが登場します。名前が似ているので間違えないように!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | 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で指定)します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 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( )が実行されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | 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はこの描画コマンドを見てアイテムの描画を行います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | 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のアイテムを変更しています。
1 2 3 4 5 6 7 8 9 | ... change( 3 , "Item X(Change)" ) ... } private fun change(position: Int, item: String) { val _adapter = rcySample.adapter as ListAdapter _adapter.notifyItemChanged(position, item) // 変更の通知(表示を更新) } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | 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つのアイテムを変更しています。
1 2 3 4 5 6 7 8 9 | ... 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) // 変更の通知(表示を更新) } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | 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つメソッドがあります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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有り側から呼ばれる下位メソッドになっています。
関連記事: