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) {
notifyDataSetChanged();
}
@Override
public void onItemRangeInserted(ObservableList<Content> contents, int i, int i1) {
notifyDataSetChanged();
}
@Override
public void onItemRangeMoved(ObservableList<Content> contents, int i, int i1, int i2) {
notifyDataSetChanged();
}
@Override
public void onItemRangeRemoved(ObservableList<Content> contents, int i, int 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を使ったほうが良さそう。