RecyclerViewのデータとビューを更新するnotify系メソッド
ユーザストリームに対応したAndroidのツイッタークライアントを作っている。
ユーザストリームを表示するためにRecyclerView
を使っているのだが、Twitter4JのTwitterStream
にセットしたリスナでStatus
を受け、RecyclerView.Adapter
にStatus
を渡した後、データセットに変更があったことをビュー側にも伝えなければならない。
最初は何も考えずにRecyclerView.Adapter#notifyDataSetChanged()
を呼んでいたものの、過去のツイートを遡って見ている時や、リツイートやふぁぼをするために選択状態にしている時でも、新しいツイートがプッシュされてくると問答無用で流されてしまう。これが嫌だったので、「一番上の子ビューが最新のツイートでない場合、またはツイートが選択状態である場合に子ビューが動いてしまう問題」をどうすれば回避できるかを試行錯誤した。そのなかで得られた知見をまとめておく。
なお、「一番上の子ビューが最新のツイートでかつ、どのツイートも選択状態でない場合」は一番上に新着のツイートを挿入して古いものを画面の下に押し出すことにする。
データ表現の要点
RecyclerView
のJavadocの冒頭には用語集がある。それによると、データの位置に関係しそうなのは次の二つ。
position
:Adapter
内でのアイテムの位置。index
: アタッチされた子ビューの番号。getChildAt(int)
で使う。
データセットは更新されたけど見た目は変えたくないということは、表示されている子ビューのposition
だけ更新されればよいということだ。
notify系メソッド
RecyclerView.Adapter#notifyDataSetChanged()
のJavadocには次のようなことが書いてある。
- データの変更イベントには
アイテムの変更
と構造の変更
の二種類がある。アイテムの変更
は、データの更新はあるものの位置の変更は起きないもの。構造の変更
はアイテムが挿入、削除、移動される時のもの。
- このイベント(
notifyDataSetChanged
のこと?)はデータセットの変更のためのものではなく、すべてのオブザーバに対し、すべてのアイテムと構造がもはや有効でないということを強制的に仮定させるものである。LayoutManager
はすべての再バインドと再レイアウトをビューに対し強制することになる。
notifyDataSetChanged()
を呼ぶとアイテムの変更
と構造の変更
とがどっちも起きるのか、と理解した。これは一番上のツイートが最新でかつ、どのツイートも選択状態でない時に呼ぶとよさそうである(最終的にはそれの代わりにsmoothScrollToPosition(0)
をよんでヌルッと動くようにした)。
アイテムの変更
をするとどうも見た目まで変わるようなので、構造の変更
だけを行うnotify~系メソッドを呼んでやることにする。新しいツイートの追加はデータセットの先頭にデータを挿入する処理なので、notifyItemInserted(int)
をメインスレッドで呼ぶことにした。これで思っていた動作になった。バックグラウンドなスレッドで呼んでいたこともあったが、ツイートを一番上に挿入する動きが実行されなかった(メインスレッドでやれ的な例外やエラーログもでない)。
おわりに
正直、なぜこれで動いているのかわからないのでもう少し調べます。特に「一番上の子ビューが最新のツイートである」というチェックのところ。