fresh digitable

めんどくさかったなってことを振り返ったり振り返らなかったりするための記録

RecyclerViewのデータとビューを更新するnotify系メソッド

ユーザストリームに対応したAndroidのツイッタークライアントを作っている。

ユーザストリームを表示するためにRecyclerViewを使っているのだが、Twitter4JのTwitterStreamにセットしたリスナでStatusを受け、RecyclerView.AdapterStatusを渡した後、データセットに変更があったことをビュー側にも伝えなければならない。

最初は何も考えずにRecyclerView.Adapter#notifyDataSetChanged()を呼んでいたものの、過去のツイートを遡って見ている時や、リツイートやふぁぼをするために選択状態にしている時でも、新しいツイートがプッシュされてくると問答無用で流されてしまう。これが嫌だったので、「一番上の子ビューが最新のツイートでない場合、またはツイートが選択状態である場合に子ビューが動いてしまう問題」をどうすれば回避できるかを試行錯誤した。そのなかで得られた知見をまとめておく。

なお、「一番上の子ビューが最新のツイートでかつ、どのツイートも選択状態でない場合」は一番上に新着のツイートを挿入して古いものを画面の下に押し出すことにする。

データ表現の要点

RecyclerViewJavadocの冒頭には用語集がある。それによると、データの位置に関係しそうなのは次の二つ。

  • position: Adapter内でのアイテムの位置。
  • index: アタッチされた子ビューの番号。getChildAt(int)で使う。

データセットは更新されたけど見た目は変えたくないということは、表示されている子ビューのpositionだけ更新されればよいということだ。

notify系メソッド

RecyclerView.Adapter#notifyDataSetChanged()Javadocには次のようなことが書いてある。

  • データの変更イベントにはアイテムの変更構造の変更の二種類がある。
    • アイテムの変更は、データの更新はあるものの位置の変更は起きないもの。
    • 構造の変更はアイテムが挿入、削除、移動される時のもの。
  • このイベント(notifyDataSetChangedのこと?)はデータセットの変更のためのものではなく、すべてのオブザーバに対し、すべてのアイテムと構造がもはや有効でないということを強制的に仮定させるものである。LayoutManagerはすべての再バインドと再レイアウトをビューに対し強制することになる。

notifyDataSetChanged()を呼ぶとアイテムの変更構造の変更とがどっちも起きるのか、と理解した。これは一番上のツイートが最新でかつ、どのツイートも選択状態でない時に呼ぶとよさそうである(最終的にはそれの代わりにsmoothScrollToPosition(0)をよんでヌルッと動くようにした)。

アイテムの変更をするとどうも見た目まで変わるようなので、構造の変更だけを行うnotify~系メソッドを呼んでやることにする。新しいツイートの追加はデータセットの先頭にデータを挿入する処理なので、notifyItemInserted(int)をメインスレッドで呼ぶことにした。これで思っていた動作になった。バックグラウンドなスレッドで呼んでいたこともあったが、ツイートを一番上に挿入する動きが実行されなかった(メインスレッドでやれ的な例外やエラーログもでない)。

おわりに

正直、なぜこれで動いているのかわからないのでもう少し調べます。特に「一番上の子ビューが最新のツイートである」というチェックのところ。