Warum wird das Hinzufügen eines OnClickListener in onBindViewHolder eines RecyclerView.Adapters als schlechte Praxis angesehen?

76

Ich habe den folgenden Code für eine RecyclerView.AdapterKlasse und es funktioniert gut:

public class MyAdapter extends RecyclerView.Adapter<MyAdapter.Viewholder> {

    private List<Information> items;
    private int itemLayout;

    public MyAdapter(List<Information> items, int itemLayout){
        this.items = items;
        this.itemLayout = itemLayout;
    }

    @Override
    public Viewholder onCreateViewHolder(ViewGroup parent, int viewType) {
        View v = LayoutInflater.from(parent.getContext()).inflate(itemLayout, parent, false);
        return new Viewholder(v);
    }

    @Override
    public void onBindViewHolder(Viewholder holder, final int position) {
        Information item = items.get(position);
        holder.textView1.setText(item.Title);
        holder.textView2.setText(item.Date);

        holder.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Toast.makeText(view.getContext(), "Recycle Click" + position, Toast.LENGTH_SHORT).show();
            }
        });

       holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
       @Override
       public boolean onLongClick(View v) {
          Toast.makeText(v.getContext(), "Recycle Click" + position, Toast.LENGTH_SHORT).show();
           return true;
       }
});
    }

    @Override
    public int getItemCount() {
        return items.size();
    }

    public class Viewholder extends RecyclerView.ViewHolder {
        public  TextView textView1;
        public TextView textView2;

        public Viewholder(View itemView) {
            super(itemView);
            textView1=(TextView) itemView.findViewById(R.id.text1);
            textView2 = (TextView) itemView.findViewById(R.id.date_row);

        }
    }
}

Ich halte es jedoch für eine schlechte Praxis, den OnClickListener in der onBindViewHolderMethode zu implementieren . Warum ist diese schlechte Praxis und was ist eine bessere Alternative?

Sujit Yadav
quelle

Antworten:

63

Der Grund, warum es besser ist, Ihre Klicklogik im ViewHolder zu handhaben, liegt darin, dass explizitere Klick-Listener möglich sind. Wie im Commonsware-Buch ausgedrückt:

Klickbare Widgets wie eine Ratingleiste in einer ListView-Zeile standen lange Zeit im Konflikt mit Klickereignissen in den Zeilen selbst. Das Abrufen von Zeilen, auf die geklickt werden kann, und von Zeileninhalten, auf die auch geklickt werden kann, wird manchmal etwas schwierig. Mit RecyclerView haben Sie eine genauere Kontrolle darüber, wie solche Dinge gehandhabt werden… weil Sie die gesamte On-Click-Verarbeitungslogik einrichten.

Durch die Verwendung des ViewHolder-Modells können Sie in einer RecyclerView viele Vorteile für die Klickbehandlung erzielen als zuvor in der ListView. Ich habe darüber in einem Blog-Beitrag geschrieben, in dem die Unterschiede verglichen wurden - https://androidessence.com/recyclerview-vs-listview

Der Grund, warum es im ViewHolder besser ist als in onBindViewHolder(), ist, dass es onBindViewHolder()für jedes Element aufgerufen wird und das Festlegen des Klick-Listeners eine unnötige Option ist, die wiederholt werden muss, wenn Sie es einmal in Ihrem ViewHolder-Konstruktor aufrufen können. Wenn Ihr Klick dann von der Position des angeklickten Elements abhängt, können Sie einfach getAdapterPosition()im ViewHolder aufrufen. Hier ist eine weitere Antwort, die zeigt, wie Sie die OnClickListeneraus Ihrer ViewHolder-Klasse heraus verwenden können.

AdamMc331
quelle
2
Um unnötige Klick-Listener zu vermeiden, haben Sie es eingerichtet! Aber können wir dies in onCreateViewHolder () implementieren, wie Brucelet vorschlägt (siehe Antwort unten)?
Sujit Yadav
1
@SujitYadav Ich nehme an, es würde den gleichen Effekt haben, da onCreateViewHolder()es nur einmal aufgerufen wird (pro ViewHolder). Ob Sie es also in Ihrem ViewHolder-Konstruktor oder in implementieren, onCreateViewHolder()liegt ganz bei Ihnen. Ich habe mir angewöhnt, es in die VH aufzunehmen, aber Sie sollten das tun, was Ihrer Meinung nach am besten lesbar ist und Ihnen in Zukunft beim Verständnis helfen wird. Vermeiden Sie einfach onBindViewHolder()aus Leistungsgründen wie von Brucelet vorgeschlagen.
AdamMc331
@Sujit @McAdam Ich mache das onCreateViewHolder()eher in als im ViewHolderKonstruktor, damit ich meine ViewHolderKlasse erstellen kann staticund keinen Verweis auf den Adapter an den übergeben muss ViewHolder. Aber letztendlich ist es meistens eine Stilwahl, da es eine Eins-zu-Eins-Entsprechung zwischen onCreateViewHolder()und geben sollte new ViewHolder().
RussHWolf
Sie müssen im Viewholder keinen Verweis auf den Adapter übergeben? Sie können getAdapterPosition()aus dem ViewHolder heraus anrufen. Siehe die Antwort, auf die ich verlinkt habe. Es sei denn, ich habe falsch verstanden, was du meintest?
AdamMc331
@FirstOne Danke für den Hinweis! Ich habe den Blog vor einiger Zeit neu geschrieben. Ich habe den Link aktualisiert. :)
AdamMc331
16

Die Methode onBindViewHolderwird jedes Mal aufgerufen, wenn Sie Ihre Ansicht mit einem Objekt verknüpfen, das gerade nicht gesehen wurde. Und jedes Mal fügen Sie einen neuen Listener hinzu.

Stattdessen sollten Sie den Klick-Listener anhängen onCreateViewHolder

Beispiel:

@Override
public Viewholder onCreateViewHolder(ViewGroup parent, int viewType) {
     View v = LayoutInflater.from(parent.getContext()).inflate(itemLayout, parent, false);
     final ViewHolder holder = new ViewHolder(v);

     holder.itemView.setOnClickListener(new View.OnClickListener() {
         @Override
         public void onClick(View v) {
             Log.d(TAG, "position = " + holder.getAdapterPosition());
         }
     });
     return holder;
}
Pavel Kozemirov
quelle
ist getAdapterPosition () die beste Methode, wenn ich die Position und das Objekt für eine bestimmte Zeile an die Aktivität sende, um CRUD-Operationen auszuführen.?bcoz, als ich getLayoutPosition () verwendet habe, funktioniert es immer noch!
adi
15

Die onCreateViewHolder()Methode wird das erste Mal aufgerufen, wenn ViewHolderjeweils a benötigt wird viewType. Die onBindViewHolder()Methode wird jedes Mal aufgerufen, wenn ein neues Element in die Ansicht gescrollt wird oder wenn sich die Daten ändern. Sie möchten teure Vorgänge vermeiden, onBindViewHolder()da dies das Scrollen verlangsamen kann. Dies ist in weniger besorgniserregend onCreateViewHolder(). Daher ist es im Allgemeinen besser, Dinge wie OnClickListeners onCreateViewHolder()so zu erstellen , dass sie nur einmal pro ViewHolderObjekt vorkommen. Sie können getLayoutPosition()den Listener anrufen , um die aktuelle Position abzurufen, anstatt das angegebene positionArgument zu übernehmen onBindViewHolder().

RussHWolf
quelle
7

Pavel lieferte ein großartiges Codebeispiel mit Ausnahme einer Zeile am Ende. Sie sollten den erstellten Inhaber zurückgeben. Nicht der neue Viewholder (v).

@Override
public Viewholder onCreateViewHolder(ViewGroup parent, int viewType) {
     View v = LayoutInflater.from(parent.getContext()).inflate(itemLayout, parent, false);
     final ViewHolder holder = new ViewHolder(v);

     holder.itemView.setOnClickListener(new View.OnClickListener() {
         @Override
         public void onClick(View v) {
             Log.d(TAG, "position = " + holder.getAdapterPosition());
         }
     });
     return holder;
}
Daria Kirsanova
quelle
3

Per https://developer.android.com/topic/performance/vitals/render , onBindViewHoldersollte seine Arbeit in „viel weniger als eine Millisekunde“ tut langsames Rendering zu verhindern.

RecyclerView: Bindung dauert zu lange

Die Bindung (dh onBindViewHolder (VH, int)) sollte sehr einfach sein und für alle außer den komplexesten Elementen weniger als eine Millisekunde dauern. Es sollte einfach POJO-Elemente aus den internen Elementdaten Ihres Adapters übernehmen und Setter für Ansichten im ViewHolder aufrufen. Wenn RV OnBindView lange dauert, stellen Sie sicher, dass Sie nur minimale Arbeit in Ihrem Bindecode leisten.

Bink
quelle
0

So implementiere ich die Klicks meiner Schaltflächen in meinem ViewHolder anstelle meines onBindViewHolder. Dieses Beispiel zeigt, wie Sie mehr als eine Schaltfläche mit einer Schnittstelle verbinden, die beim Auffüllen von Zeilen nicht mehr Objekte generiert.

Das Beispiel ist auf Spanisch und auf Kotlin , aber ich bin sicher, dass die Logik verständlich ist.

/**
 * Created by Gastón Saillén on 26 December 2019
 */
class DondeComprarRecyclerAdapter(val context:Context,itemListener:RecyclerViewClickListener):RecyclerView.Adapter<BaseViewHolder<*>>() {

    interface RecyclerViewClickListener {
        fun comoLlegarOnClick(v: View?, position: Int)
        fun whatsappOnClick(v:View?,position: Int)
    }

    companion object{
        var itemClickListener: RecyclerViewClickListener? = null
    }

    init {
        itemClickListener = itemListener
    }

    private var adapterDataList = mutableListOf<Institucion>()

   fun setData(institucionesList:MutableList<Institucion>){
        this.adapterDataList = institucionesList
    }

    fun getItemAt(position:Int):Institucion = adapterDataList[position]

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder<*> {
        val view = LayoutInflater.from(context)
            .inflate(R.layout.dondecomprar_row, parent, false)
        return PuntosDeVentaViewHolder(view)
    }

    override fun getItemCount(): Int {
        return if(adapterDataList.size > 0) adapterDataList.size else 0
    }

    override fun onBindViewHolder(holder: BaseViewHolder<*>, position: Int) {
        val element = adapterDataList[position]
        when(holder){
            is PuntosDeVentaViewHolder -> holder.bind(element)
            else -> throw IllegalArgumentException()
        }

    }

    inner class PuntosDeVentaViewHolder(itemView: View):BaseViewHolder<Institucion>(itemView),View.OnClickListener{

        override fun bind(item: Institucion) {
            itemView.txtTitleDondeComprar.text = item.titulo
            itemView.txtDireccionDondeComprar.text = item.direccion
            itemView.txtHorarioAtencDondeComprar.text = item.horario
            itemView.btnComoLlegar.setOnClickListener(this)
            itemView.btnWhatsapp.setOnClickListener(this)
        }

        override fun onClick(v: View?) {
            when(v!!.id){
                R.id.btnComoLlegar -> {
                    itemClickListener?.comoLlegarOnClick(v, adapterPosition)
                }

                R.id.btnWhatsapp -> {
                    itemClickListener?.whatsappOnClick(v,adapterPosition)
                }
            }
        }
    }
}

Und der BaseViewHolder, der in jedem Adapter implementiert werden soll

/**
 * Created by Gastón Saillén on 27 December 2019
 */
abstract class BaseViewHolder<T>(itemView: View) : RecyclerView.ViewHolder(itemView) {
    abstract fun bind(item: T)
}
Gastón Saillén
quelle
0

Ich hatte ein kleines Problem, das ich in den Antworten teilen möchte, wenn auch jemand anderes damit konfrontiert ist. Ich hatte Bild und Text in Recycleview als Cardview anzuzeigen. Daher sollte mein Code gemäß den Empfehlungen wie folgt lauten.

@Override
    public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View itemView = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.books_item_row, parent, false);

          final MyViewHolder holder = new MyViewHolder(itemView);
        holder.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
      public void onClick(View v) {
  Toast.makeText(getActivity(), "Recycle Click", Toast.LENGTH_LONG).show();
            }
        });
         return holder;
    }

Wenn ich jedoch in der Recycling-Ansicht auf die Karte klicke, funktioniert sie nicht, da sich die Elementansicht unter dem Bild befindet. Daher habe ich den Code wie folgt leicht geändert.

 @Override
        public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            View itemView = LayoutInflater.from(parent.getContext())
                    .inflate(R.layout.books_item_row, parent, false);

              final MyViewHolder holder = new MyViewHolder(itemView);
            holder.thumbnail.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    //Log.d(TAG, "position = " + holder.getAdapterPosition());
                        Toast.makeText(getActivity(), "Recycle Click", Toast.LENGTH_LONG).show();
                    }
            });
                 return holder;
        }

Das heißt, anstelle der Objektansicht muss die Person jetzt auf die Miniaturansicht oder das Bild klicken.

Abdul Wahid
quelle
0

Sie können dies auch auf diese Weise tun.

MainActivity-Klasse

In dieser Vielzahl von Schnittstellentriggern können Sie dies erreichen ...

Adapterklasse

P Sekhar
quelle