为啥在 RecyclerView.Adapter 的 onBindViewHolder 中添加 OnClickListener 被认为是不好的做法?

Posted

技术标签:

【中文标题】为啥在 RecyclerView.Adapter 的 onBindViewHolder 中添加 OnClickListener 被认为是不好的做法?【英文标题】:Why is adding an OnClickListener inside onBindViewHolder of a RecyclerView.Adapter considered bad practice?为什么在 RecyclerView.Adapter 的 onBindViewHolder 中添加 OnClickListener 被认为是不好的做法? 【发布时间】:2016-02-24 01:43:42 【问题描述】:

我有以下代码用于 RecyclerView.Adapter 类,它工作正常:

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);

        
    

但是,我认为在 onBindViewHolder 方法中实现 OnClickListener 是一种不好的做法。为什么这是不好的做法,有什么更好的选择?

【问题讨论】:

【参考方案1】:

在 ViewHolder 中处理点击逻辑更好的原因是它允许更明确的点击侦听器。正如 Commonsware 书中所述:

ListView 行中的可点击小部件(如 RatingBar)长期以来一直与行本身的点击事件发生冲突。获取可以单击的行以及也可以单击的行内容有时会有点棘手。使用 RecyclerView,您可以更明确地控制此类事情的处理方式……因为您是设置所有点击处理逻辑的人。

通过使用 ViewHolder 模型,与以前在 ListView 中相比,在 RecyclerView 中进行点击处理可以获得很多好处。我在一篇比较差异的博文中写到了这一点 - https://androidessence.com/recyclerview-vs-listview

至于为什么在 ViewHolder 中而不是在 onBindViewHolder() 中更好,那是因为 onBindViewHolder() 为每个项目调用,并且设置点击侦听器是一个不必要的选项,当你可以调用它一次时重复你的 ViewHolder 构造函数。然后,如果您的点击响应取决于所点击项目的位置,您可以简单地从 ViewHolder 中调用 getAdapterPosition()。 Here 是我给出的另一个答案,它演示了如何在 ViewHolder 类中使用 OnClickListener

【讨论】:

为了避免不必要的点击监听设置了!但是我们可以像 Brucelet 建议的那样在 onCreateViewHolder() 中实现它吗(见下面的答案)。 @SujitYadav 我想它会产生相同的效果,因为onCreateViewHolder() 只被调用一次(每个 ViewHolder)所以无论你在 ViewHolder 构造函数中还是在 onCreateViewHolder() 中实现它都取决于你个人喜好。我已经养成了把它放在 VH 中的习惯,但是你应该做你认为最易读的,并且会帮助你将来理解的东西。出于性能原因(如 brucelet 建议),请避免使用 onBindViewHolder() @Sujit @McAdam 我更喜欢在onCreateViewHolder() 而不是ViewHolder 构造函数中这样做,这样我就可以使我的ViewHolderstatic 并且不需要传递对适配器插入ViewHolder。但这最终主要是一种风格选择,因为onCreateViewHolder()new ViewHolder() 之间应该是一一对应的。 您不需要在viewholder中传递对适配器的引用吗?您可以从 ViewHolder 内部调用 getAdapterPosition()。请参阅我链接到的答案。除非我误解了你的意思? @FirstOne 感谢您的通知!不久前我重写了博客。我已经更新了链接。 :)【参考方案2】:

每次当你将你的视图绑定到刚刚还没有看到的对象时,都会调用 onBindViewHolder 方法。并且每次你都会添加一个新的监听器。

您应该做的是在onCreateViewHolder 上附加点击侦听器

示例:

@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;

【讨论】:

是 getAdapterPosition() 的最佳使用方式,如果我将特定行的位置和对象发送到 Activity 以执行 CRUD 操作。当我使用 getLayoutPosition() 时,它仍然有效! 很好的答案。顺便提一下,getAdapterPosition() 只能放在 onClick() 方法里面,不能放在listner外面,否则位置会是-1。那是因为当 onCreateViewHolder() 被调用时,viewholder 还没有被附加到视图上;当单击视图时,视图必须已附加到视图,因此它可以工作。此外,getAdapterPosition() 将是 deprecated【参考方案3】:

onCreateViewHolder() 方法将被调用前几次,每个viewType 都需要ViewHolderonBindViewHolder() 方法将在每次新项目滚动到视图中或更改其数据时调用。您希望避免在onBindViewHolder() 中进行任何昂贵的操作,因为它会减慢您的滚动速度。在onCreateViewHolder() 中,这不是一个问题。因此,通常最好在onCreateViewHolder() 中创建像OnClickListeners 这样的东西,这样它们每个ViewHolder 对象只发生一次。您可以在侦听器内部调用getLayoutPosition() 以获取当前位置,而不是将position 提供给onBindViewHolder() 的参数。

【讨论】:

【参考方案4】:

Pavel provided 很棒的代码示例,除了最后一行。您应该返回已创建的持有人。不是新的 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;

【讨论】:

【参考方案5】:

根据https://developer.android.com/topic/performance/vitals/render,onBindViewHolder 应该在“不到一毫秒”的时间内完成其工作,以防止渲染缓慢。

RecyclerView:绑定耗时过长

Bind(即onBindViewHolder(VH, int))应该很简单,而且 除了最复杂的以外,所有时间都不到一毫秒 项目。它只是应该从适配器的内部获取 POJO 项目 项目数据,并在 ViewHolder 中的视图上调用设置器。如果房车 OnBindView 需要很长时间,请确认您正在做的最少 在您的绑定代码中工作。

【讨论】:

【参考方案6】:

这就是我在 ViewHolder 而不是 onBindViewHolder 中实现按钮点击的方式。这个例子展示了如何将多个按钮与一个界面绑定,这样在填充行时不会生成更多的对象。

示例是西班牙语和 Kotlin,但我相信逻辑是可以理解的。

/**
 * 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)
                
            
        
    

以及要在每个适配器中实现的 BaseViewHolder

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

【讨论】:

【参考方案7】:

我遇到了一个小问题,如果其他人也面临这个问题,我想在答案中分享。 我有图像和文本在 Recycleview 中显示为 Cardview。因此,根据建议,我的代码应如下所示。

@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;
    

但是,当我在回收视图中单击卡片时,它将不起作用,因为 itemview 在图像下方。因此,我稍微更改了代码如下。

 @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;
        

也就是说,现在人们必须点击缩略图或图像,而不是 itemview。

【讨论】:

【参考方案8】:

你也可以这样做..

MainActivity class

在这多种类型的界面触发器中你可以实现这个...

Adapter class

【讨论】:

以上是关于为啥在 RecyclerView.Adapter 的 onBindViewHolder 中添加 OnClickListener 被认为是不好的做法?的主要内容,如果未能解决你的问题,请参考以下文章

RecyclerView.Adapter优化了吗?

RecyclerView 知识梳理 - Adapter

java 在RecyclerView Adapter onCreateViewHolder()获取查看位置

RecyclerView.Adapter.notifyItemMoved(0,1) 滚动屏幕

Android RecyclerView Adapter notifyItemChanged() 获取 IllegalStateException 已经在池中

我无法从另一个带有 recyclerview.adaper 的片段中打开带有 recyclerview.adapter 的片段