デリゲートアダプター-理由と方法

私が関与したほとんどすべてのプロジェクトで、要素のリスト(リボン)を表示する必要がありましたが、これらの要素はさまざまなタイプでした。 多くの場合、タスクはメインアダプター内で解決され、getItemViewType()のinstanceOfを介して要素のタイプが決定されます。 タイプ2または3のテープの場合、このアプローチはそれ自体を正当化するように見えます...またはそうではありませんか? 明日、さらに複雑なロジックに従って、さらにいくつかの型を導入する要求が来たらどうなりますか?







この記事では、DelegateAdapterパターンがこの問題をどのように解決するかを示したいと思います。 LayoutContainerを使用したKotlinでの実装を見ると、パターンに精通していることが興味深い場合があります。



問題



例から始めましょう。 2種類のデータ(説明付きのテキストと写真)を含むリボンを表示するタスクがあるとします。



型のモデルを作成します。
public interface IViewModel {}
      
      





 public class TextViewModel implements IViewModel { @NonNull public final String title; @NonNull public final String description; public TextViewModel(@NonNull String title, @NonNull String description) { this.title = title; this.description = description; } }
      
      





 public class ImageViewModel implements IViewModel { @NonNull public final String title; @NonNull public final @DrawableRes int imageRes; public ImageViewModel(@NonNull String title, @NonNull int imageRes) { this.title = title; this.imageRes = imageRes; } }
      
      







典型的なアダプタは次のようになります
 public class BadAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { private static final int TEXT_VIEW_TYPE = 1; private static final int IMAGE_VIEW_TYPE = 2; private List<IViewModel> items; private View.OnClickListener imageClickListener; public BadAdapter(List<IViewModel> items, View.OnClickListener imageClickListener) { this.items = items; this.imageClickListener = imageClickListener; } public int getItemViewType(int position) { IViewModel item = items.get(position); if (item instanceof TextViewModel) return TEXT_VIEW_TYPE; if (item instanceof ImageViewModel) return IMAGE_VIEW_TYPE; throw new IllegalArgumentException( "Can't find view type for position " + position); } @Override public RecyclerView.ViewHolder onCreateViewHolder( ViewGroup parent, int viewType) { RecyclerView.ViewHolder holder; LayoutInflater inflater = LayoutInflater.from(parent.getContext()); if (viewType == TEXT_VIEW_TYPE) { holder = new TextViewHolder( inflater.inflate(R.layout.text_item, parent, false)); } else if (viewType == IMAGE_VIEW_TYPE) { holder = new ImageViewHolder( inflater.inflate(R.layout.image_item, parent, false), imageClickListener); } else { throw new IllegalArgumentException( "Can't create view holder from view type " + viewType); } return holder; } @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { int viewType = getItemViewType(position); if (viewType == TEXT_VIEW_TYPE) { TextViewHolder txtViewHolder = (TextViewHolder) holder; TextViewModel model = (TextViewModel) items.get(position); txtViewHolder.tvTitle.setText(model.title); txtViewHolder.tvDescription.setText(model.description); } else if (viewType == IMAGE_VIEW_TYPE) { ImageViewHolder imgViewHolder = (ImageViewHolder) holder; ImageViewModel model = (ImageViewModel) items.get(position); imgViewHolder.tvTitle.setText(model.title); imgViewHolder.imageView.setImageResource(model.imageRes); } else { throw new IllegalArgumentException( "Can't create bind holder fro position " + position); } } @Override public int getItemCount() { return items.size(); } private static class TextViewHolder extends RecyclerView.ViewHolder { private TextView tvTitle; private TextView tvDescription; private TextViewHolder(View parent) { super(parent); tvTitle = parent.findViewById(R.id.tv_title); tvDescription = parent.findViewById(R.id.tv_description); } } private static class ImageViewHolder extends RecyclerView.ViewHolder { private TextView tvTitle; private ImageView imageView; private ImageViewHolder(View parent, View.OnClickListener listener) { super(parent); tvTitle = parent.findViewById(R.id.tv_title); imageView = parent.findViewById(R.id.img_bg); imageView.setOnClickListener(listener); } } }
      
      







この実装の欠点は、DRYおよびSOLIDの原則(単一責任およびオープンクローズ)に違反していることです。 これを確認するには、2つの要件を追加するだけで十分です。新しいデータ型(チェックボックス)と、チェックボックスと画像のみが存在する別のテープを入力します。



2番目のテープに同じアダプターを使用するか、新しいアダプターを作成するかという選択に直面しています。 選択するソリューションに関係なく、コードを変更する必要があります(ほぼ同じですが、場所が異なります)。 新しいVIEW_TYPE、新しいViewHolderを追加し、メソッドgetItemViewType()、onCreateViewHolder()およびonBindViewHolder()を編集する必要があります。



1つのアダプターを残すことにした場合、変更は終了します。 しかし、将来、新しいロジックを持つ新しいデータ型が2番目のテープにのみ追加される場合、最初のテープには追加の機能があり、変更はされていませんが、テストする必要があります。



新しいアダプターを作成することにした場合、単純に大量のコードが重複します。



既製のソリューション



Delegate Adapterパターンはこの問題にうまく対処します-すでに記述されたコードを変更する必要はなく、既存のアダプターを簡単に再利用できます。



Kotlinでのプロジェクトの作成に関する JoãoIgnacioの一連の記事を読んでいるときに、初めてパターンに出会いました。 Juanの実装、およびハブに照らされたソリューション-RendererRecyclerViewAdapter -ViewTypeについての知識がすべてのアダプターに分散されているため、私は好ましくありません。



詳細な説明
Joanの決定では、ViewTypeを登録する必要があります。



 object AdapterConstants { val NEWS = 1 val LOADING = 2 }
      
      





ViewTypeインターフェースを実装するモデルを作成します。



 class SomeModel : ViewType { override fun getViewType() = AdapterConstants.NEWS }
      
      





DelegateAdapter cを定数で登録します。



 delegateAdapters.put(AdapterConstants.NEWS, NewsDelegateAdapter(listener))
      
      





したがって、データ型のロジックは3つのクラス(定数、モデル、および登録が行われる場所)に分散されます。 さらに、誤って同じ値を持つ2つの定数を作成しないようにする必要があります。これは、RendererRecyclerViewAdapterを使用したソリューションでは非常に簡単です。



 class SomeModel implements ItemModel { public static final int TYPE = 0; //  0   -  ? @NonNull private final String mTitle; ... @Override public int getType() { return TYPE; } }
      
      







これらのアプローチはどちらもHans Dorfman AdapterDelegatesライブラリに基づいています。これは私が気に入っていますが、アダプターを作成する必要があるという欠点があります。 この部分は「ボイラープレート」であり、省略できます。



別の解決策



コードは言葉よりも自分自身のために話すでしょう。 2つのデータ型(テキストと画像)で同じテープを実装してみましょう。 LayoutContainerを使用してKotlinで実装を記述します(以下で詳細に説明します)。



テキスト用のアダプターを作成します。



 class TxtDelegateAdapter : KDelegateAdapter<TextViewModel>() { override fun onBind(item: TextViewModel, viewHolder: KViewHolder) = with(viewHolder) { tv_title.text = item.title tv_description.text = item.description } override fun isForViewType(items: List<*>, position: Int) = items[position] is TextViewModel override fun getLayoutId(): Int = R.layout.text_item }
      
      





写真用アダプター:



 class ImageDelegateAdapter(private val clickListener: View.OnClickListener) : KDelegateAdapter<ImageViewModel>() { override fun onBind(item: ImageViewModel, viewHolder: KViewHolder) = with(viewHolder) { tv_title.text = item.title img_bg.setOnClickListener(clickListener) img_bg.setImageResource(item.imageRes) } override fun isForViewType(items: List<*>, position: Int) = items[position] is ImageViewModel override fun getLayoutId(): Int = R.layout.image_item }
      
      





メインアダプターの作成場所にアダプターを登録します。



  val adapter = CompositeDelegateAdapter.Builder<IViewModel>() .add(ImageDelegateAdapter(onImageClick)) .add(TextDelegateAdapter()) .build() recyclerView.layoutManager = LinearLayoutManager(this) recyclerView.adapter = adapter
      
      





タスクを解決するために行う必要があるのはこれだけです。 従来の実装と比較してコードがどれだけ少ないかに注目してください。 さらに、このアプローチにより、新しいデータ型を簡単に追加し、DelegateAdapterを相互に組み合わせることができます。



新しいデータ型(チェックボックス)を追加する必要があることを想像してみましょう。 何をする必要がありますか?



モデルを作成:



 class CheckViewModel(val title: String, var isChecked: Boolean): IViewModel
      
      





書き込みアダプター:



 class CheckDelegateAdapter : KDelegateAdapter<CheckViewModel>() { override fun onBind(item: CheckViewModel, viewHolder: KViewHolder) = with(viewHolder.check_box) { text = item.title isChecked = item.isChecked setOnCheckedChangeListener { _, isChecked -> item.isChecked = isChecked } } override fun onRecycled(viewHolder: KViewHolder) { viewHolder.check_box.setOnCheckedChangeListener(null) } override fun isForViewType(items: List<*>, position: Int) = items[position] is CheckViewModel override fun getLayoutId(): Int = R.layout.check_item }
      
      





アダプターに行を追加します。



  val adapter = CompositeDelegateAdapter.Builder<IViewModel>() .add(ImageDelegateAdapter(onImageClick)) .add(TextDelegateAdapter()) .add(CheckDelegateAdapter()) .build()
      
      





テープの新しいデータ型は、レイアウト、ViewHolder、およびバインディングロジックです。 また、提案されたアプローチはすべて同じクラスに属しているため、気に入っています。 一部のプロジェクトでは、ViewHoldersとViewBindersは別々のクラスに配置され、レイアウトの膨張はメインアダプターで発生します。 タスクを想像してみてください-テープのいずれかのデータ型のフォントサイズを変更するだけです。 ViewHolderに移動すると、findViewById(R.id.description)が表示されます。 説明をクリックすると、Ideaは、そのIDを持つビューを持つ35のレイアウトを提供します。 次に、メインアダプター、ParentAdapter、onCreateViewHolderメソッドの順に移動し、最後に、40個の要素の中から目的のスイッチを見つける必要があります。



「問題」セクションには、別のテープの作成に関する要件がありました。 デリゲートアダプタを使用すると、タスクは簡単になります。CompositeAdapterを作成し、必要なタイプのDelegateAdapterを登録するだけです。



 val newAdapter = CompositeDelegateAdapter.Builder<IViewModel>() .add(ImageDelegateAdapter(onImageClick)) .add(CheckDelegateAdapter()) .build()
      
      





すなわち アダプタは互いに独立しており、簡単に組み合わせることができます。 別の利点は、ハンドラー(onlickListener)を渡す便利さです。 BadAdapter(上記の例)では、ハンドラーがアダプターに渡され、アダプターは既にそれをViewHolderに渡しました。 これにより、コードの接続性が向上します。 提案されたソリューションでは、ハンドラはコンストラクタを介して、それらを必要とするクラスにのみ渡されます。



実装



基本実装(KotlinおよびLayoutContainerなし)には、4つのクラスが必要です。



インターフェースDelegateAdapter
 public interface IDelegateAdapter<VH extends RecyclerView.ViewHolder, T> { @NonNull RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType); void onBindViewHolder(@NonNull VH holder, @NonNull List<T> items, int position); void onRecycled(VH holder); boolean isForViewType(@NonNull List<?> items, int position); }
      
      







メインアダプター
 public class CompositeDelegateAdapter<T> extends RecyclerView.Adapter<RecyclerView.ViewHolder> { private static final int FIRST_VIEW_TYPE = 0; protected final SparseArray<IDelegateAdapter> typeToAdapterMap; protected final @NonNull List<T> data = new ArrayList<>(); protected CompositeDelegateAdapter( @NonNull SparseArray<IDelegateAdapter> typeToAdapterMap) { this.typeToAdapterMap = typeToAdapterMap; } @Override public final int getItemViewType(int position) { for (int i = FIRST_VIEW_TYPE; i < typeToAdapterMap.size(); i++) { final IDelegateAdapter delegate = typeToAdapterMap.valueAt(i); //noinspection unchecked if (delegate.isForViewType(data, position)) { return typeToAdapterMap.keyAt(i); } } throw new NullPointerException( "Can not get viewType for position " + position); } @Override public final RecyclerView.ViewHolder onCreateViewHolder( ViewGroup parent, int viewType) { return typeToAdapterMap.get(viewType) .onCreateViewHolder(parent, viewType); } @Override public final void onBindViewHolder( RecyclerView.ViewHolder holder, int position) { final IDelegateAdapter delegateAdapter = typeToAdapterMap.get(getItemViewType(position)); if (delegateAdapter != null) { //noinspection unchecked delegateAdapter.onBindViewHolder(holder, data, position); } else { throw new NullPointerException( "can not find adapter for position " + position); } } @Override public void onViewRecycled(RecyclerView.ViewHolder holder) { //noinspection unchecked typeToAdapterMap.get(holder.getItemViewType()) .onRecycled(holder); } public void swapData(@NonNull List<T> data) { this.data.clear(); this.data.addAll(data); notifyDataSetChanged(); } @Override public final int getItemCount() { return data.size(); } public static class Builder<T> { private int count; private final SparseArray<IDelegateAdapter> typeToAdapterMap; public Builder() { typeToAdapterMap = new SparseArray<>(); } public Builder<T> add( @NonNull IDelegateAdapter<?, ? extends T> delegateAdapter) { typeToAdapterMap.put(count++, delegateAdapter); return this; } public CompositeDelegateAdapter<T> build() { if (count == 0) { throw new IllegalArgumentException("Register at least one adapter"); } return new CompositeDelegateAdapter<>(typeToAdapterMap); } } }
      
      







ご覧のとおり、魔法ではなく、onBind、onCreate、onRecycledの呼び出しを委任するだけです(Hans DorfmanによるAdapterDelegatesの実装と同じです)。



次に、基本的なViewHolderとDelegateAdpaterを作成して、もう少し「定型」を削除します。



Baseviewholder
 public class BaseViewHolder extends RecyclerView.ViewHolder { private ItemInflateListener listener; public BaseViewHolder(View parent) { super(parent); } public final void setListener(ItemInflateListener listener) { this.listener = listener; } public final void bind(Object item) { listener.inflated(item, itemView); } interface ItemInflateListener { void inflated(Object viewType, View view); } }
      
      







Basedelegateadapter
 public abstract class BaseDelegateAdapter <VH extends BaseViewHolder, T> implements IDelegateAdapter<VH,T> { abstract protected void onBindViewHolder( @NonNull View view, @NonNull T item, @NonNull VH viewHolder); @LayoutRes abstract protected int getLayoutId(); @NonNull abstract protected VH createViewHolder(View parent); @Override public void onRecycled(VH holder) { } @NonNull @Override public final RecyclerView.ViewHolder onCreateViewHolder( @NonNull ViewGroup parent, int viewType) { final View inflatedView = LayoutInflater .from(parent.getContext()) .inflate(getLayoutId(), parent, false); final VH holder = createViewHolder(inflatedView); holder.setListener(new BaseViewHolder.ItemInflateListener() { @Override public void inflated(Object viewType, View view) { onBindViewHolder(view, (T) viewType, holder); } }); return holder; } @Override public final void onBindViewHolder( @NonNull VH holder, @NonNull List<T> items, int position) { ((BaseViewHolder) holder).bind(items.get(position)); } }
      
      







これで、ほとんど上記の例のように、アダプターを作成できるようになります。



TextDelegateAdapterの例
 public class TextDelegateAdapter extends BaseDelegateAdapter<TextDelegateAdapter.TextViewHolder, TextViewModel> { @Override protected void onBindViewHolder(@NonNull View view, @NonNull TextViewModel item, @NonNull TextViewHolder viewHolder) { viewHolder.tvTitle.setText(item.title); viewHolder.tvDescription.setText(item.description); } @Override protected int getLayoutId() { return R.layout.text_item; } @Override protected TextViewHolder createViewHolder(View parent) { return new TextViewHolder(parent); } @Override public boolean isForViewType(@NonNull List<?> items, int position) { return items.get(position) instanceof TextViewModel; } final static class TextViewHolder extends BaseViewHolder { private TextView tvTitle; private TextView tvDescription; private TextViewHolder(View parent) { super(parent); tvTitle = parent.findViewById(R.id.tv_title); tvDescription = parent.findViewById(R.id.tv_description); } } }
      
      







ViewHoldersを自動的に作成するには(Kotlinでのみ機能します)、次の3つのことを実行します。



  1. 合成ビューリンクインポート用の接続プラグイン



     apply plugin: 'kotlin-android-extensions'
          
          



  2. 実験的なオプションを許可する



      androidExtensions { experimental = true }
          
          



  3. LayoutContainerインターフェイスを実装する

    デフォルトでは、リンクはアクティビティとフラグメントに対してのみキャッシュされます。 詳細はこちら


これで、基本クラスを記述できます。



 abstract class KDelegateAdapter<T> : BaseDelegateAdapter<KDelegateAdapter.KViewHolder, T>() { abstract fun onBind(item: T, viewHolder: KViewHolder) final override fun onBindViewHolder(view: View, item: T, viewHolder: KViewHolder) { onBind(item, viewHolder) } override fun createViewHolder(parent: View?): KViewHolder { return KViewHolder(parent) } class KViewHolder(override val containerView: View?) : BaseViewHolder(containerView), LayoutContainer }
      
      





短所



  1. viewTypeを決定する必要があるときにアダプターを検索するには、平均N / 2が必要です。Nは登録済みアダプターの数です。 そのため、多数のアダプターを使用すると、ソリューションの動作が多少遅くなります。
  2. 同じViewModelにサブスクライブする2つのアダプター間で競合が発生する場合があります。
  3. クラスはKotlinでのみコンパクトです。



おわりに



このアプローチは、複雑なリストと同種のリストの両方で実証済みです。アダプターを記述すると、文字通り10行のコードになりますが、アーキテクチャーにより、既存のクラスを変更せずにテープを拡張および複雑化できます。



誰かがソースコードを必要とする場合のために、 プロジェクトへのリンクを提供します 。 フィードバックをお待ちしております。



All Articles