何かあやまりなどがあればお教えください
ObjectAdapterやPresenterSelector、Presenterなどは、多分AndroidTVのアプリコードを読むとたくさん出てくるコードだと思います。
そろそろComposeのTV対応がいい感じになると思うのですが、既存のコードもみていく必要があります。結構モバイルの実装をしていた方からすると独特なので、メモとして書いておきます。
Presenterという単語が出てくるので、MVPパターンかな?とか思うとめちゃくちゃ混乱しますが全然違いそうです。
もし、あなたがRecyclerViewを知っているという前提であれば、Leanbackの内部でRecyclerViewを使っているだけなので、コードを読むと結構かんたんに理解することができます。
コード例
これがObjectAdapterやPresenterを使うときのコード例です。
rowsAdapterというのを作って、そこにデータを入れて、それをsetAdapterしています。
ArrayObjectAdapterは入れ子になっていて、縦のAdapterの中に横のAdapterが入っています。
private fun loadRows() {
// ArrayObjectAdapterを作る
val rowsAdapter = ArrayObjectAdapter(ListRowPresenter())
// 本来は`val listRowAdapter = ArrayObjectAdapter(cardPresenter)`だけで済むが、
// 理解のために、わざとPresenterSelectorを使う処理を書いている。
// これを使うことによって複数のデータ型に対応できる。
val cardPresenter = MovieCardPresenter()
val presenters: Array<Presenter> = arrayOf(cardPresenter)
val presenterSelector = object : PresenterSelector() {
override fun getPresenter(item: Any?): Presenter {
if (item is Movie) {
return cardPresenter
}
throw IllegalArgumentException()
}
override fun getPresenters(): Array<Presenter> {
return presenters
}
}
val listRowAdapter = ArrayObjectAdapter(presenterSelector)
listRowAdapter.add(MovieList.list[3])
listRowAdapter.add(MovieList.list[4])
rowsAdapter.add(ListRow(listRowAdapter))
// ここでFragmentのsetAdapter()を呼んでAdapterをセットする。
adapter = rowsAdapter
}
実装から理解する
図にするとちょっと難しそうに見えるんですが、RecyclerViewから使われる実装的な観点だと以下のようでした。
- ObjectAdapter: RecyclerViewのAdapterのgetItemCount()で使われて、データの数を渡す。データそのものを管理。またPresenterSelectorを保持する。
- PresenterSelector: データに対するRecyclerViewのItemViewType毎にPresenterを返す
- Presenter: 一つのViewTypeに対するViewの生成、データのバインドを管理
ItemBridgeAdapterというLeanback内で使われるクラスの実装を見るとどういうことなのかがとてもわかりやすいと思います
単にRecyclerViewのAdapterの処理がObjectAdapter、PresenterSelector、Presenterに移譲されているということがわかるかと思います。
この考え方をベースにコードを見ていくとかなりサクサク読めていきそうでした。
実際のコード
public class ItemBridgeAdapter extends RecyclerView.Adapter implements FacetProviderAdapter {
...
private ObjectAdapter mAdapter;
private ArrayList<Presenter> mPresenters = new ArrayList<Presenter>();
...
@Override
public int getItemCount() {
// ここでObjectAdapterからデータ数を取得する
return mAdapter != null ? mAdapter.size() : 0;
}
...
@Override
public int getItemViewType(int position) {
// ここでObjectAdapterからPresenterSelectorを取得する。
PresenterSelector presenterSelector = mPresenterSelector != null
? mPresenterSelector : mAdapter.getPresenterSelector();
// ObjectAdapterからデータの取得
Object item = mAdapter.get(position);
// データからPresenterSelectorを使ってPresenterを取得
Presenter presenter = presenterSelector.getPresenter(item);
// ItemTypeは保持しているpresenterのindexで計算する。
int type = mPresenters.indexOf(presenter);
if (type < 0) {
// まだ管理されていないPresenterが来たら、リストに入れて、そのindexを返す
mPresenters.add(presenter);
type = mPresenters.indexOf(presenter);
if (DEBUG) Log.v(TAG, "getItemViewType added presenter " + presenter + " type " + type);
onAddPresenter(presenter, type);
if (mAdapterListener != null) {
mAdapterListener.onAddPresenter(presenter, type);
}
}
return type;
}
以下の図はそれぞれのクラスの関係を表しています。
なぜこのような仕組みがあるのか?
RecyclerViewだけでいいじゃんって思うんですが、おそらく、以下のようにフォーカスしたら大きくなったり、影が出たりなどのTVとしての統一した体験を作りやすくするために作られている仕組みだと思います。