为什么在RecyclerView.Adapter的onBindViewHolder方法中添加OnClickListener被认为是不良实践?

84

我有以下用于 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是一种不好的做法。这是为什么,有更好的替代方案吗?

8个回答

73
使用ViewHolder处理点击逻辑更好的原因是它允许更明确的点击监听器。正如在Commonsware书中所述:
“可点击的小部件,例如RatingBar,在ListView行中长期以来一直与行本身上的点击事件发生冲突。有时获取既可点击的行,又可点击的行内容会变得有些棘手。通过RecyclerView,您可以更明确地控制这种事情的处理方式......因为您是设置所有on-click处理逻辑的人。”
通过使用ViewHolder模式,您可以在RecyclerView中获得比ListView中更多的点击处理优势。我在一篇博客文章中写到了这一点,比较了两者之间的差异-https://androidessence.com/recyclerview-vs-listview

关于为什么在ViewHolder中而不是onBindViewHolder()中更好的原因,是因为onBindViewHolder()会为每个项目调用一次,而在ViewHolder构造函数中调用一次点击监听器比重复设置一个不必要的选项更好。然后,如果您的点击响应取决于所点击的项目位置,则可以从ViewHolder内部简单地调用getAdapterPosition()这里是我提供的另一个答案,演示了如何在ViewHolder类中使用OnClickListener


2
为了避免不必要的点击监听器设置,我们可以在onCreateViewHolder()中实现Brucelet建议的方法(请参见下面的答案)。 - Sujit Yadav
1
@SujitYadav 我想它会产生相同的效果,因为 onCreateViewHolder() 只会被调用一次(每个 ViewHolder),所以你是在 ViewHolder 构造函数中实现它还是在 onCreateViewHolder() 中实现它,这取决于你个人的喜好。我已经养成了把它放在 VH 中的习惯,但你应该做你认为最易读且有助于你未来理解的事情。只要像 brucelet 建议的那样避免使用 onBindViewHolder() 以提高性能即可。 - AdamMc331
@Sujit @McAdam 我倾向于在 onCreateViewHolder() 中完成,而不是在 ViewHolder 构造函数中完成,这样我可以将我的 ViewHolder 类设为 static,而不需要将适配器的引用传递给 ViewHolder。但这主要是一种风格选择,因为 onCreateViewHolder()new ViewHolder() 应该有一对一的对应关系。 - RussHWolf
你不需要在viewholder中传递adapter的引用吗?你可以从ViewHolder内部调用getAdapterPosition()。请参见我链接的答案。除非我误解了你的意思? - AdamMc331
@FirstOne 感谢您的提醒!我之前重写了博客,已经更新了链接。 :) - AdamMc331
显示剩余2条评论

19

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()时,它仍然可以工作! - adi
很好的答案。顺便提醒一下,getAdapterPosition()应该仅放在onClick()方法内部,而不是监听器外部,否则位置将为-1。这是因为当调用onCreateViewHolder()时,viewholder尚未附加到视图;而当viewholder被点击时,viewholder必须已经附加到视图,因此它可以工作。此外,getAdapterPosition()将被弃用。 - vainquit

16

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


7

Pavel提供了一个非常好的编程示例,除了最后一行。你应该返回创建的holder,而不是新的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;
}

4
根据https://developer.android.com/topic/performance/vitals/render,为了防止渲染缓慢,onBindViewHolder的工作应该在“远远少于一毫秒”的时间内完成。

RecyclerView:绑定时间过长

绑定(即onBindViewHolder(VH,int))应该非常简单,并且对于所有但最复杂的项,都应该花费远远少于一毫秒的时间。它只需从适配器的内部项数据中获取POJO项,并在ViewHolder中的视图上调用setter。如果RV OnBindView花费了很长时间,请确保您在绑定代码中进行最小化的工作。


0

我遇到了一个小问题,如果其他人也遇到了同样的问题,我想在答案中分享一下。 我有一张图片和一段文本要在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。


0

0

这是我如何在我的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)
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接