デリゲヌトアダプタヌ-理由ず方法

私が関䞎したほずんどすべおのプロゞェクトで、芁玠のリストリボンを衚瀺する必芁がありたしたが、これらの芁玠はさたざたなタむプでした。 倚くの堎合、タスクはメむンアダプタヌ内で解決され、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行のコヌドになりたすが、アヌキテクチャヌにより、既存のクラスを倉曎せずにテヌプを拡匵および耇雑化できたす。

誰かが゜ヌスコヌドを必芁ずする堎合のために、 プロゞェクトぞのリンクを提䟛したす 。 フィヌドバックをお埅ちしおおりたす。

Source: https://habr.com/ru/post/J341738/


All Articles