I was exploring RecyclerView
and I was surprised to see that RecyclerView
does not have onItemClickListener()
.
I've two question.
I want to know why Google removed onItemClickListener()
?
Is there a performance issue or something else?
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?
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.
The RecyclerView library provides three layout managers, which handle the most common layout situations: LinearLayoutManager arranges the items in a one-dimensional list.
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.
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.
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
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
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)
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 }
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); } }
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With