Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why doesn't RecyclerView have onItemClickListener()?

I was exploring RecyclerView and I was surprised to see that RecyclerView does not have onItemClickListener().

I've two question.

Main Question

I want to know why Google removed onItemClickListener()?

Is there a performance issue or something else?

Secondary Question

I solved my problem by writing onClick in my RecyclerView.Adapter:

public static class ViewHolder extends RecyclerView.ViewHolder implements OnClickListener {      public TextView txtViewTitle;     public ImageView imgViewIcon;      public ViewHolder(View itemLayoutView) {         super(itemLayoutView);         txtViewTitle = (TextView) itemLayoutView.findViewById(R.id.item_title);         imgViewIcon = (ImageView) itemLayoutView.findViewById(R.id.item_icon);     }      @Override     public void onClick(View v) {      } } 

Is this ok / is there any better way?

like image 595
T_V Avatar asked Jul 22 '14 10:07

T_V


People also ask

What is ViewHolder RecyclerView?

A ViewHolder describes an item view and metadata about its place within the RecyclerView. RecyclerView. Adapter implementations should subclass ViewHolder and add fields for caching potentially expensive View.

How many layouts are used in RecyclerView?

The RecyclerView library provides three layout managers, which handle the most common layout situations: LinearLayoutManager arranges the items in a one-dimensional list.


2 Answers

tl;dr 2016 Use RxJava and a PublishSubject to expose an Observable for the clicks.

public class ReactiveAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> {     String[] mDataset = { "Data", "In", "Adapter" };      private final PublishSubject<String> onClickSubject = PublishSubject.create();      @Override      public void onBindViewHolder(final ViewHolder holder, int position) {         final String element = mDataset[position];          holder.itemView.setOnClickListener(new View.OnClickListener() {             @Override             public void onClick(View v) {                onClickSubject.onNext(element);             }         });     }      public Observable<String> getPositionClicks(){         return onClickSubject.asObservable();     } } 

Original Post:

Since the introduction of ListView, onItemClickListener has been problematic. The moment you have a click listener for any of the internal elements the callback would not be triggered but it wasn't notified or well documented (if at all) so there was a lot of confusion and SO questions about it.

Given that RecyclerView takes it a step further and doesn't have a concept of a row/column, but rather an arbitrarily laid out amount of children, they have delegated the onClick to each one of them, or to programmer implementation.

Think of Recyclerview not as a ListView 1:1 replacement but rather as a more flexible component for complex use cases. And as you say, your solution is what google expected of you. Now you have an adapter who can delegate onClick to an interface passed on the constructor, which is the correct pattern for both ListView and Recyclerview.

public static class ViewHolder extends RecyclerView.ViewHolder implements OnClickListener {      public TextView txtViewTitle;     public ImageView imgViewIcon;     public IMyViewHolderClicks mListener;      public ViewHolder(View itemLayoutView, IMyViewHolderClicks listener) {         super(itemLayoutView);         mListener = listener;         txtViewTitle = (TextView) itemLayoutView.findViewById(R.id.item_title);         imgViewIcon = (ImageView) itemLayoutView.findViewById(R.id.item_icon);         imgViewIcon.setOnClickListener(this);         itemLayoutView.setOnClickListener(this);     }      @Override     public void onClick(View v) {         if (v instanceof ImageView){            mListener.onTomato((ImageView)v);         } else {            mListener.onPotato(v);         }     }      public static interface IMyViewHolderClicks {         public void onPotato(View caller);         public void onTomato(ImageView callerImage);     }  } 

and then on your adapter

public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> {     String[] mDataset = { "Data" };     @Override    public MyAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {        View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.my_layout, parent, false);         MyAdapter.ViewHolder vh = new ViewHolder(v, new MyAdapter.ViewHolder.IMyViewHolderClicks() {             public void onPotato(View caller) { Log.d("VEGETABLES", "Poh-tah-tos"); };            public void onTomato(ImageView callerImage) { Log.d("VEGETABLES", "To-m8-tohs"); }         });         return vh;     }      // Replace the contents of a view (invoked by the layout manager)      @Override      public void onBindViewHolder(ViewHolder holder, int position) {         // Get element from your dataset at this position          // Replace the contents of the view with that element          // Clear the ones that won't be used         holder.txtViewTitle.setText(mDataset[position]);     }       // Return the size of your dataset (invoked by the layout manager)      @Override      public int getItemCount() {          return mDataset.length;     }    ... 

Now look into that last piece of code: onCreateViewHolder(ViewGroup parent, int viewType) the signature already suggest different view types. For each one of them you'll require a different viewholder too, and subsequently each one of them can have a different set of clicks. Or you can just create a generic viewholder that takes any view and one onClickListener and applies accordingly. Or delegate up one level to the orchestrator so several fragments/activities have the same list with different click behaviour. Again, all flexibility is on your side.

It is a really needed component and fairly close to what our internal implementations and improvements to ListView were until now. It's good that Google finally acknowledges it.

like image 189
MLProgrammer-CiM Avatar answered Oct 25 '22 07:10

MLProgrammer-CiM


Why the RecyclerView has no onItemClickListener

The RecyclerView is a toolbox, in contrast of the old ListView it has less build in features and more flexibility. The onItemClickListener is not the only feature being removed from ListView. But it has lot of listeners and method to extend it to your liking, it's far more powerful in the right hands ;).

In my opinion the most complex feature removed in RecyclerView is the Fast Scroll. Most of the other features can be easily re-implemented.

If you want to know what other cool features RecyclerView added read this answer to another question.

Memory efficient - drop-in solution for onItemClickListener

This solution has been proposed by Hugo Visser, an Android GDE, right after RecyclerView was released. He made a licence-free class available for you to just drop in your code and use it.

It showcase some of the versatility introduced with RecyclerView by making use of RecyclerView.OnChildAttachStateChangeListener.

Edit 2019: kotlin version by me, java one, from Hugo Visser, kept below

Kotlin / Java

Create a file values/ids.xml and put this in it:

<?xml version="1.0" encoding="utf-8"?> <resources>     <item name="item_click_support" type="id" /> </resources> 

then add the code below to your source

Kotlin

Usage:

recyclerView.onItemClick { recyclerView, position, v ->     // do it } 

(it also support long item click and see below for another feature I've added).

implementation (my adaptation to Hugo Visser Java code):

typealias OnRecyclerViewItemClickListener = (recyclerView: RecyclerView, position: Int, v: View) -> Unit typealias OnRecyclerViewItemLongClickListener = (recyclerView: RecyclerView, position: Int, v: View) -> Boolean  class ItemClickSupport private constructor(private val recyclerView: RecyclerView) {      private var onItemClickListener: OnRecyclerViewItemClickListener? = null     private var onItemLongClickListener: OnRecyclerViewItemLongClickListener? = null      private val attachListener: RecyclerView.OnChildAttachStateChangeListener = object : RecyclerView.OnChildAttachStateChangeListener {         override fun onChildViewAttachedToWindow(view: View) {             // every time a new child view is attached add click listeners to it             val holder = [email protected](view)                     .takeIf { it is ItemClickSupportViewHolder } as? ItemClickSupportViewHolder              if (onItemClickListener != null && holder?.isClickable != false) {                 view.setOnClickListener(onClickListener)             }             if (onItemLongClickListener != null && holder?.isLongClickable != false) {                 view.setOnLongClickListener(onLongClickListener)             }         }          override fun onChildViewDetachedFromWindow(view: View) {          }     }      init {         // the ID must be declared in XML, used to avoid         // replacing the ItemClickSupport without removing         // the old one from the RecyclerView         this.recyclerView.setTag(R.id.item_click_support, this)         this.recyclerView.addOnChildAttachStateChangeListener(attachListener)     }      companion object {         fun addTo(view: RecyclerView): ItemClickSupport {             // if there's already an ItemClickSupport attached             // to this RecyclerView do not replace it, use it             var support: ItemClickSupport? = view.getTag(R.id.item_click_support) as? ItemClickSupport             if (support == null) {                 support = ItemClickSupport(view)             }             return support         }          fun removeFrom(view: RecyclerView): ItemClickSupport? {             val support = view.getTag(R.id.item_click_support) as? ItemClickSupport             support?.detach(view)             return support         }     }      private val onClickListener = View.OnClickListener { v ->         val listener = onItemClickListener ?: return@OnClickListener         // ask the RecyclerView for the viewHolder of this view.         // then use it to get the position for the adapter         val holder = this.recyclerView.getChildViewHolder(v)         listener.invoke(this.recyclerView, holder.adapterPosition, v)     }      private val onLongClickListener = View.OnLongClickListener { v ->         val listener = onItemLongClickListener ?: return@OnLongClickListener false         val holder = this.recyclerView.getChildViewHolder(v)         return@OnLongClickListener listener.invoke(this.recyclerView, holder.adapterPosition, v)     }      private fun detach(view: RecyclerView) {         view.removeOnChildAttachStateChangeListener(attachListener)         view.setTag(R.id.item_click_support, null)     }      fun onItemClick(listener: OnRecyclerViewItemClickListener?): ItemClickSupport {         onItemClickListener = listener         return this     }      fun onItemLongClick(listener: OnRecyclerViewItemLongClickListener?): ItemClickSupport {         onItemLongClickListener = listener         return this     }  }  /** Give click-ability and long-click-ability control to the ViewHolder */ interface ItemClickSupportViewHolder {     val isClickable: Boolean get() = true     val isLongClickable: Boolean get() = true }  // Extension function fun RecyclerView.addItemClickSupport(configuration: ItemClickSupport.() -> Unit = {}) = ItemClickSupport.addTo(this).apply(configuration)  fun RecyclerView.removeItemClickSupport() = ItemClickSupport.removeFrom(this)  fun RecyclerView.onItemClick(onClick: OnRecyclerViewItemClickListener) {     addItemClickSupport { onItemClick(onClick) } } fun RecyclerView.onItemLongClick(onLongClick: OnRecyclerViewItemLongClickListener) {     addItemClickSupport { onItemLongClick(onLongClick) } } 

(Remember you also need to add an XML file, see above this section)

Bonus feature of Kotlin version

Sometimes you do not want all the items of the RecyclerView to be clickable.

To handle this I've introduced the ItemClickSupportViewHolder interface that you can use on your ViewHolder to control which item is clickable.

Example:

class MyViewHolder(view): RecyclerView.ViewHolder(view), ItemClickSupportViewHolder {     override val isClickable: Boolean get() = false     override val isLongClickable: Boolean get() = false } 

Java

Usage:

ItemClickSupport.addTo(mRecyclerView)         .setOnItemClickListener(new ItemClickSupport.OnItemClickListener() {     @Override     public void onItemClicked(RecyclerView recyclerView, int position, View v) {         // do it     } }); 

(it also support long item click)

Implementation (comments added by me):

public class ItemClickSupport {     private final RecyclerView mRecyclerView;     private OnItemClickListener mOnItemClickListener;     private OnItemLongClickListener mOnItemLongClickListener;     private View.OnClickListener mOnClickListener = new View.OnClickListener() {         @Override         public void onClick(View v) {             if (mOnItemClickListener != null) {                 // ask the RecyclerView for the viewHolder of this view.                 // then use it to get the position for the adapter                 RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(v);                 mOnItemClickListener.onItemClicked(mRecyclerView, holder.getAdapterPosition(), v);             }         }     };     private View.OnLongClickListener mOnLongClickListener = new View.OnLongClickListener() {         @Override         public boolean onLongClick(View v) {             if (mOnItemLongClickListener != null) {                 RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(v);                 return mOnItemLongClickListener.onItemLongClicked(mRecyclerView, holder.getAdapterPosition(), v);             }             return false;         }     };     private RecyclerView.OnChildAttachStateChangeListener mAttachListener             = new RecyclerView.OnChildAttachStateChangeListener() {         @Override         public void onChildViewAttachedToWindow(View view) {             // every time a new child view is attached add click listeners to it             if (mOnItemClickListener != null) {                 view.setOnClickListener(mOnClickListener);             }             if (mOnItemLongClickListener != null) {                 view.setOnLongClickListener(mOnLongClickListener);             }         }          @Override         public void onChildViewDetachedFromWindow(View view) {          }     };      private ItemClickSupport(RecyclerView recyclerView) {         mRecyclerView = recyclerView;         // the ID must be declared in XML, used to avoid         // replacing the ItemClickSupport without removing         // the old one from the RecyclerView         mRecyclerView.setTag(R.id.item_click_support, this);         mRecyclerView.addOnChildAttachStateChangeListener(mAttachListener);     }      public static ItemClickSupport addTo(RecyclerView view) {         // if there's already an ItemClickSupport attached         // to this RecyclerView do not replace it, use it         ItemClickSupport support = (ItemClickSupport) view.getTag(R.id.item_click_support);         if (support == null) {             support = new ItemClickSupport(view);         }         return support;     }      public static ItemClickSupport removeFrom(RecyclerView view) {         ItemClickSupport support = (ItemClickSupport) view.getTag(R.id.item_click_support);         if (support != null) {             support.detach(view);         }         return support;     }      public ItemClickSupport setOnItemClickListener(OnItemClickListener listener) {         mOnItemClickListener = listener;         return this;     }      public ItemClickSupport setOnItemLongClickListener(OnItemLongClickListener listener) {         mOnItemLongClickListener = listener;         return this;     }      private void detach(RecyclerView view) {         view.removeOnChildAttachStateChangeListener(mAttachListener);         view.setTag(R.id.item_click_support, null);     }      public interface OnItemClickListener {          void onItemClicked(RecyclerView recyclerView, int position, View v);     }      public interface OnItemLongClickListener {          boolean onItemLongClicked(RecyclerView recyclerView, int position, View v);     } } 

How it works (why it's efficient)

This class works by attaching a RecyclerView.OnChildAttachStateChangeListener to the RecyclerView. This listener is notified every time a child is attached or detached from the RecyclerView. The code use this to append a tap/long click listener to the view. That listener ask the RecyclerView for the RecyclerView.ViewHolder which contains the position.

This is more efficient then other solutions because it avoid creating multiple listeners for each view and keep destroying and creating them while the RecyclerView is being scrolled.

You could also adapt the code to give you back the holder itself if you need more.

Final remark

Keep in mind that it's COMPLETELY fine to handle it in your adapter by setting on each view of your list a click listener, like other answer proposed.

It's just not the most efficient thing to do (you create a new listener every time you reuse a view) but it works and in most cases it's not an issue.

It is also a bit against separation of concerns cause it's not really the Job of the Adapter to delegate click events.

like image 39
Daniele Segato Avatar answered Oct 25 '22 06:10

Daniele Segato