どくぴーの備忘録

真面目なことを書こうとするクソメガネのブログ。いつ投げ捨てられるのかは不明

ObservableList#OnListChangedCallbackでListViewやRecyclerViewのAdapterを更新する

TL;DR

  • ObservableList#OnListChangedCallbackでObservableListに格納されているデータの変更を通知できる
  • 変更を通知できるので「通知のあったitemのみViewを更新する」と言った処理も可能
    • RecyclerViewなら各コールバックメソッドに対応した変更通知が使える
    • ListViewには変更通知として notifyDataSetChanged() しかないので同様のことをしたい場合、getViewを独自に叩くことになるのでActivity/FragmentにObservableList#OnListChangedCallbackを配置するのが早そう
    • チャットUI等の実装にはListViewではなくてRecyclerViewを使ったほうがViewの更新回数が減りそう

ObservableList

DataBindingで実装されているObservableなコレクションの一種で、イメージ的には「データ変更を通知する機能を備えたList」。DataBindingにおける各データの通知方法(BaseObservable、ObservableField等)はこの辺を参照

developer.android.com

ObservableListは、コールバックにOnListChangedCallbackを持っており、(RecyclerViewについてきた)SortedListよろしくリスト内の要素の変更を通知することができる。というわけで、ListViewやRecyclerViewのAdapterに持たせるListにコールバックを実装したObservableListを与えることで、リストの要素が変更されたときや追加されたときなどにその変更をUIに反映させることができる。

Listの変更をListViewやRecyclerViewに通知したい

ListViewの場合

色々見ているとAdapterのコンストラクタ内でObservableList#OnListChangedCallbackをセットしていることが多そう。Adapterの変更通知は notifyDataSetChanged() しかないので、基本的にOnListChangedCallbackのどのコールバックメソッドが走っても(表示されている)全部の要素に対して getView() が実行され、描画されることになる。

public class ContentsAdapter extends ArrayAdapter<Content> {

    public ContentsAdapter(Context context, ObservableArrayList<Content> objects) {
        super(context, 0, objects);

        objects.addOnListChangedCallback(new ObservableList.OnListChangedCallback<ObservableList<Content>>() {
            @Override
            public void onChanged(ObservableList<Content> contents) {
                // リストそのものが変更されたとき
                notifyDataSetChanged();
            }

            @Override
            public void onItemRangeChanged(ObservableList<Content> contents, int i, int i1) {
                // iを始点としてi1までの範囲の要素が変更されたとき
                notifyDataSetChanged();
            }

            @Override
            public void onItemRangeInserted(ObservableList<Content> contents, int i, int i1) {
                // iを始点としてi1までの範囲に要素が挿入されたとき
                notifyDataSetChanged();
            }

            @Override
            public void onItemRangeMoved(ObservableList<Content> contents, int i, int i1, int i2) {
                // iからi1へi2の数だけの要素が移動したとき
                notifyDataSetChanged();
            }

            @Override
            public void onItemRangeRemoved(ObservableList<Content> contents, int i, int i1) {
                // iを始点としてi1までの範囲の要素が削除されたとき
                notifyDataSetChanged();
            }
        });
    }

    // 以下省略...
}

変更が通知された要素のみViewを再描画したい場合、 notifyDataSetChanged() せずに、直接AdapterのgetViewを叩くことで指定のViewのみを更新できる。しかしCallbackをAdapterのコンストラクタ内で定義するとうまくいかないのでListViewが表示されるActivity/FragmentでCallbackを定義してあげる必要があるし、実行するにしても getFirstVisiblePosition()getLastVisiblePosition() で表示されている範囲内の要素か判定する必要がありそう。 「ListViewの中に更にListViewやRecyclerViewがあって、再描画するとListView/RecyclerViewの表示が崩れたり一瞬消えたりする」場合に使うと言ったケースはあるかもしれない。

stackoverflow.com

RecyclerViewの場合

ObservableList#OnListChangedCallbackのセットはListViewのAdapterと同様、Adapterのコンストラクタ内でやっているのが多い感じ。ただ、RecyclerViewのAdapterはListViewのAdapterとは違って、notify系メソッドが用途別に揃っているっぽい。

  • notifyDataSetChanged()
    • データセットが変更されたことを登録されているすべてのObserverに通知する。表示されているItem全体を更新する
    • この場合のデータセットの変更は「既存の要素が全て有効ではなくなる」ことを想定させるもの。なのでこれを実行するとLayoutManagerは表示される範囲のViewの再描画を行う。
  • notifyItemChanged(int position)
    • 指定したpositionのitemが変更されたことを登録されているすべてのobserverに通知する。そのpositionのItemのみを更新する
  • notifyItemInserted(int position)
    • 指定したpositionのitemが挿入されたことを登録されているすべてのobserverに通知する。そのpositionのItemのみを更新する
  • notifyItemRemoved(int position)
    • 指定したpositionのitemが削除されたことを登録されているすべてのobserverに通知する。そのpositionのItemのみを削除する
  • notifyItemMoved(int fromPosition, int toPosition)
    • fromPositionのitemがtoPositionに移動したことを登録されているすべてのobserverに通知する。移動に関係のあるItemのみを更新する
  • notifyItemRangeChanged(int positionStart, int itemCount)
    • positionStartからitemCountの範囲だけのitemが変更されたことを登録されているすべてのobserverに通知する。該当範囲のItemのみを更新する
  • notifyItemRangeInserted(int positionStart, int itemCount)
    • positionStartからitemCountの範囲だけのitemが挿入されたことを登録されているすべてのobserverに通知する。該当範囲のItemのみを更新する
  • notifyItemRangeRemoved(int positionStart, int itemCount)
    • positionStartからitemCountの範囲だけのitemが削除されたことを登録されているすべてのobserverに通知する。該当範囲のItemのみを削除する

(引数にObject payloadを与えるものもあるが今回は特に使う機会がないので見なかったことにする)

RecyclerView.Adapter | Android Developers

qiita.com

この中からObservableList#OnListChangedCallbackのそれぞれのコールバックメソッドに相当するものを選ぶと、

  • onChanged - notifyDataSetChanged
  • onItemRangeChanged - notifyItemRangeChanged
  • onItemRangeInserted - notifyItemRangeInserted
  • onItemRangeMoved - notifyItemRangeRemoved

になる。onItemRangeRemoved(T sender, int positionStart, int itemCount)だけは対応するnotify系メソッドがないというか、notifyItemRangeMovedが存在しないので困ったところだが、

for (int i = 0; i < itemCount; i++) { 
        notifyItemMoved(fromPosition + i, toPosition + i); 
} 

でforでちまちま回すか notifyItemRangeChanged(fromPosition, toPosition + itemCount) で範囲変更として扱うかで対応できそう。(ここに関しては試していないので自信がない)

public class ContentsRecyclerAdapter extends RecyclerView.Adapter<ContentsRecyclerAdapter.BindingViewHolder> {

    private ObservableArrayList<Content> objects;

    public ContentsRecyclerAdapter(ObservableArrayList<Content> objects) {
        this.objects = objects;
        this.objects.addOnListChangedCallback(new ObservableList.OnListChangedCallback<ObservableList<Content>>() {
            @Override
            public void onChanged(ObservableList<Content> contents) {
                notifyDataSetChanged();
            }

            @Override
            public void onItemRangeChanged(ObservableList<Content> contents, int i, int i1) {
                notifyItemRangeChanged(i, i1);
            }

            @Override
            public void onItemRangeInserted(ObservableList<Content> contents, int i, int i1) {
                notifyItemRangeInserted(i, i1);
            }

            @Override
            public void onItemRangeMoved(ObservableList<Content> contents, int i, int i1, int i2) {
                for (int j = 0; j < i2; i++) {
                    notifyItemMoved(i + j, i1 + j);
                }

            }

            @Override
            public void onItemRangeRemoved(ObservableList<Content> contents, int i, int i1) {
                notifyItemRangeRemoved(i, i1);
            }
        });
    }

    // 以下省略...
}

このようにしてあげることでListViewのAdapterのnotifyDataSetChanged()ではできなかった「変更があった要素のみViewを更新する」ことが可能になる。ついでに何かアニメーションもしてくれる。このようにすると、単純にListViewに比べてもViewの描画回数が減るので、ポーリングでリストの内容を更新したり、チャットUIの実装を行うときにDataBindingを扱う場合、RecyclerViewを使ったほうが良さそう。