GithubやStackOverflow見た感じだと、みんな色んな実装を試しているような感じで、今回は僕もこんな感じで考えてみたよっていうのを出してみようと思って作ってみた。
完全新規の考え方という訳でもなく、似たようなライブラリもいくつかあると思うが、今回はそこの考えに至った経緯を中心に何か書けると良いなあといった感じ。
作ろうと思った経緯
あるアプリの開発をしていた時に、一つのリストで項目に応じたレイアウトを出し分けて表示するような実装をする際に色々苦労して実装した過去があった。
いくつかの実装例やライブラリを参考にして実装したのだが、色々しっくりこないところがあったので自分で作ってみることにした。
複雑なRecyclerView実装のつらいところ
実装例
- https://gist.github.com/mheras/0908873267def75dc746
- https://github.com/kk-java/HeaderFooterRecyclerView/blob/master/headerfooterrecyclerview/src/main/java/com/liucanwen/app/headerfooterrecyclerview/HeaderAndFooterRecyclerViewAdapter.java
これらの例は目的がはっきりしているし、コードに対して批判的なわけではない。
(むしろここまで目的が明確ならこの方法が僕が作ったものより簡単に使えるはず)
ただもし表題のような何種類もセクションを分けてViewを扱うような場合でも、この実装するとつらみが発生する。
つらいところ
上の実装例で何種類ものViewを扱おうとした時に発生するつらいところはこんな感じ
-
getItemViewType(position)
など色々な箇所で、項目の内容を見て生成するViewHolderを振り分けている - 種類が増えるた場合などの長期的な運用を考えるとロジックは肥大化してしまうので機能拡張しづらい
- 仮にタッチイベントを制御したい場合、Adapterごとにタッチイベントの挙動を変えたいでもタッチイベント内で処理をふり分ける必要がある
一つのAdapterでやろうとするとコードが複雑化してしまうので、何らかに切り出す必要がありそうだった。
設計的に目指したこと
複雑なリストを作る場合でもある程度統一したルールで実装できるようにする
- 親Adapterを作り、それが複数の子Adapterを持つ仕組みにする
- 親Adapterは子Adapterしか追加できない
- 子Adapterごとの項目管理は、子Adapter自身がやる
- 子Adapterは子Adapterを持つことはできない(持てるのは親のみ)
- 子Adapter単体でもRecyclerViewのAdapterとして扱える
- リストを表現するものである以上は単体でも利用できるようにする
- switch文などで振り分け処理を書かない
- Adapterが項目の内容を見て振り分けるというのは極力やらせない
- ある程度汎用的に扱えるようにしたい
- 拡張性があること
- 難しいリストを表示する場合でも同じようなコードの組み合わせで実現できるようにしたい
成果物
RecyclerViewAdapters
特徴
一つ一つのセクションは基本的にLocalAdapter単位で完結している
その項目はどのようなレイアウトリソースを参照して、どのような方法でデータを反映しているのかをLocalAdapter単位で管理できる。
合わせて項目のクラスもLocalAdapter単位で組み合わせられるので、セクションごとに異なる文脈のレイアウトを構築できる。
こうすることで、複雑なリストを組み合わせるためにそれ用のAdapterを新規に作る必要はないし、再利用もできる作りになっている。
LocalAdapterは組み合わせても、単独でも使える
LocalAdapterの実装クラスはRecyclerView.Adapterを継承しているので、単独でもRecyclerViewに使える。(もし独自のLocalAdapter作る時はBaseLocalAdapterを継承してね)
これらを単独、もしくは組み合わせてリストを表現する
親
- CompositeRecyclerAdapter
- 以下で説明するアダプタを組み合わせることができる
- CompositeRecyclerAdapter同士で組み合わせることはできない
子
CompositeRecyclerAdapterで扱えるようにするにはLocalAdapterを実装する
- ItemAdapter
- ListViewでいうArrayAdapterみたいなやつ
- 特定の項目を配列で管理している
- タップなどのイベントハンドラを設定できる
- ダブルタップやロングタップにも対応
- ViewItem
- 一つのViewを表現する
- Header・Footerのようなリスト内の単一なViewを表現するのに使う
- ViewItemAdapter
- ViewItemを配列で管理しているアダプタ
- 基本的にはViewItem単独でリストを表現できるが、これはViewItemの集まりを一つのセクションとして表現したい時に使う(Header・Footerがそれぞれ複数ある場合の管理などに使える)
使い方
Gradle
repositories {
maven { url 'https://jitpack.io' }
}
dependencies {
compile 'com.github.chuross:recyclerview-adapters:1.1.0'
}
基本的な使い方
先ほどのHeader・Footerを表現するリストをこのライブラリで作ってみる
項目ごとにLocalAdapterを定義して組み合わせるだけ!
CompositeRecyclerAdapter compositeAdapter = new CompositeRecyclerAdapter();
// AdapterIdはCompositeRecyclerAdapterにおけるItemViewTypeのようなもの
// 同じLocalAdapterのクラスを使う場合はそれぞれ違うAdapterIdを振る
ViewItemAdapter header = new ViewItemAdapter(this) {
@Override
public int getAdapterId() {
return R.id.header;
}
};
header.add(new ViewItem(this, R.layout.item_header_1));
header.add(new ViewItem(this, R.layout.item_header_2));
// 同じようにgetAdapterIdをオーバーライドする
ViewItemAdapter footer = new ViewItemAdapter(this) { ... };
footer.add(new ViewItem(this, R.layout.item_footer_1));
ItemAdapter itemAdapter = new ItemAdapter<String, SOMETHING_VIEW_HOLDER>(this) {
// AdapterId, onCreateViewHolder, onBindViewHolderを実装する
}
itemAdapter.add("hoge");
itemAdapter.add("fuga");
itemAdapter.add("piyo");
// Adapter単位でリスナーをセットする
itemAdapter.setOnItemClickListener(new OnItemClickListener() {
void onItemClicked(RecyclerView.ViewHolder holder, int position, String item) { ... }
});
// これらのアダプタを組み合わせる
compositeAdapter.add(header);
compositeAdapter.add(itemAdapter);
compositeAdapter.add(footer);
// RecyclerViewの設定
RecyclerView list = (RecyclerView) findViewById(this, R.id.list);
list.setLayoutManager(new LinearLayoutManager(this));
list.setAdapter(compositeAdapter); // list.setAdapter(itemAdapter); これでも動く
...
// イベントなど別のタイミングで変更を加える場合でも対象のアダプタに大して追加・削除するだけ
// 内容が更新されるとCompositeRecyclerAdapterも更新されて画面に反映される
itemAdapter.add("hello world.");
CompositeRecylerAdapterのindexから子要素を取得する
CompositeRecyclerAdapterを使っている場合に、RecyclerViewが表示しているインデックスから、どのLocalAdapterの何番目の要素かの情報を取得できる
LocalAdapterItem localItem = compositeAdapter.getLocalAdapterItem(100);
localItem.getLocalAdapter(); // RecyclerViewの100番目の項目で使われているLocalAdapterを取得できる
localItem.getLocalAdapterPosition(); // LocalAdapterの何番目のアイテムか取得できる
ただ基本的にItemAdapterを使っていれば、そこでクリック時のイベント処理ができるのであんまし使う機会はないかも
SpanSizeLookupを設定する
愚直に書くと恐らく汚い振り分け処理を書いてしまう箇所だが、このライブラリを使うと比較的簡単に設定できる。
SpanSizeLookupBuilderを使うと、CompositeRecyclerAdapterを使った場合にLocalAdapterごとにSpanSizeを設定することができる
GridLayoutManager gridLayoutManager = new GridLayoutManager(this, SPAN_SIZE);
// LocalAdapterインタフェースを継承しているインスタンス単位で設定できる
gridLayoutManager.setSpanSizeLookup(
new SpanSizeLookupBuilder(this, compositeAdapter)
.bind(viewItem1, SPAN_SIZE)
.bind(viewItemAdapter, SPAN_SIZE)
.bind(itemAdapter1, 2, 1) // 画面の縦、横でSpanSizeを指定したい時に使う
.bind(itemAdapter2, SPAN_SIZE)
.build()
);
// CompositeRecyclerAdapterを使わないと反映されないよ!
recyclerView.setAdapter(compositeRecyclerAdapter);
まとめ
Adapterを組み合わせて構築できるため、基本的にはViewItemとItemAdapterの組み合わせで大体のリストは表現できると思う。
ViewItemをあるセクションに分けて、イベントに合わせて動的に変更したい場合はViewItemAdapterを使用すると良い。
ItemAdapterの中で項目のプロパティによって、Viewの出し分けをするような場合は今回の実装でも結局処理の振り分けは発生してしまうかもしれない。
その場合は別のライブラリを使うか、事前にViewItemで振り分けてしまってViewItemAdapterで管理するなど工夫が必要かも。
TODO
- 気持ちだけでがっと作ってしまったのでテスト追加
- README.md