RecyclerView Item Click Listener the Right Way
Asked Answered
C

13

50

I use RecyclerView adapter to display data inside an activity, I want to implement onClickListener inside the activity, currently, I am setting onClickListener inside adapter as usual which works fine.

public void onBindViewHolder(MyHolder holder, final int position) {
    final Listdata data = listdata.get(position);
    holder.vname.setText(data.getName());

    holder.vname.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            Toast.makeText(activity, "clicked on " +position, Toast.LENGTH_SHORT).show();
        }
    });
}

However I want to implement it inside activity so I have greater control. This doesn't serve my purpose. I think it'll be useful for a lot of us.

Cordite answered 22/4, 2018 at 18:21 Comment(4)
In this answer> https://mcmap.net/q/355281/-cant-go-from-one-activity-to-another-in-recycler-view I've created an easy to use plugin class adapter (ClickableAdapter) that adds is just like the setOnClickListener of listviews but adapted for RecyclerViews. So you can call adaoter,setOnClickListener directly in the activity and handle the clicks thereSergei
You can set clickListener in onCreateViewHolder instead of onBindViewHolder. Check out my answer. onBindViewHolder is called every time recyclerView is scrolled.Prurigo
if you are open to using Kotlin then maybe something like this would work for you? The listener is set in the adapter, but the function used in the listener is declared in the FragmentVeasey
For reference, the official Google recommended way to implement "item click listeners" in RecyclerView is in this talk: youtu.be/KhLVD6iiZQs?t=2048Algorithm
S
101

You need to check this tutorial here for better understanding on how you can achieve the behaviour that you want.

In case of handling the onClickListener from your activity you need to work based on a callback implementation with an interface. Pass the interface from the activity to your adapter and then call the callback function from your adapter when some items are clicked.

Here's a sample implementation from the tutorial.

Let us first have the interface.

public interface OnItemClickListener {
    void onItemClick(ContentItem item);
}

You need to modify your adapter to take the listener as the parameter like the one stated below.

private final List<ContentItem> items;
private final OnItemClickListener listener;

public ContentAdapter(List<ContentItem> items, OnItemClickListener listener) {
    this.items = items;
    this.listener = listener;
}

Now in your onBindViewHolder method, set the click listener.

@Override public void onBindViewHolder(ViewHolder holder, int position) {
    holder.bind(items.get(position), listener);
}

public void bind(final ContentItem item, final OnItemClickListener listener) {
    ...
    itemView.setOnClickListener(new View.OnClickListener() {
        @Override public void onClick(View v) {
            listener.onItemClick(item);
        }
    });
}

Now setting the adapter in your RecyclerView.

recycler.setAdapter(new ContentAdapter(items, new ContentAdapter.OnItemClickListener() {
    @Override public void onItemClick(ContentItem item) {
        Toast.makeText(getContext(), "Item Clicked", Toast.LENGTH_LONG).show();
    }
}));

So the whole adapter code looks like the following.

public class ContentAdapter extends RecyclerView.Adapter<ContentAdapter.ViewHolder> {

    public interface OnItemClickListener {
        void onItemClick(ContentItem item);
    }

    private final List<ContentItem> items;
    private final OnItemClickListener listener;

    public ContentAdapter(List<ContentItem> items, OnItemClickListener listener) {
        this.items = items;
        this.listener = listener;
    }

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

    @Override public void onBindViewHolder(ViewHolder holder, int position) {
        holder.bind(items.get(position), listener);
    }

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

    static class ViewHolder extends RecyclerView.ViewHolder {

        private TextView name;
        private ImageView image;

        public ViewHolder(View itemView) {
            super(itemView);
            name = (TextView) itemView.findViewById(R.id.name);
            image = (ImageView) itemView.findViewById(R.id.image);
        }

        public void bind(final ContentItem item, final OnItemClickListener listener) {
            name.setText(item.name);
            Picasso.with(itemView.getContext()).load(item.imageUrl).into(image);
            itemView.setOnClickListener(new View.OnClickListener() {
                @Override public void onClick(View v) {
                    listener.onItemClick(item);
                }
            });
        }
    }
}
Shalondashalt answered 22/4, 2018 at 18:43 Comment(8)
Your answer is pretty good, I have something like this asked can you please show my question ? #54507774Lipp
Thanks for the mention. Definitely, I will check the question and will get back to you when I get some time.Shalondashalt
inside this itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { listener.onItemClick(item); } }); how to call the update adapter ?Maki
Just call notifyDataSetChanged(), that should do.Shalondashalt
This creates a new listener each time onBindViewHolder is called, which can cause noticeable performance degradation on scrolling large lists because of GCAlgorithm
Great explanation. I use it this implemantation in my ViewPager's ReyclerView. My ViewPager includes another RecyclerView and i can catch the click and set this click position on ViewPager's PagerAdapter. Thank you.Yalu
It's better to set the click listener inside the body of the ViewHolder's constructor.Thickknee
For reference, the official Google recommended way to implement "item click listeners" in RecyclerView is in this talk: youtu.be/KhLVD6iiZQs?t=2048Algorithm
P
23

Registering clickListener inside onCreateViewHolder instead of onBindViewHolder is more performant since you only add listener when a view is created not ever time recyclerView is scrolled.

And i use ListAdapter with DiffUtil callback instead of RecyclerViewAdapter

abstract class BaseListAdapter<ItemType>(
    callBack: DiffUtil.ItemCallback<ItemType> = DefaultItemDiffCallback(),
    private inline val onItemClicked: ((ItemType, Int) -> Unit)? = null
) : ListAdapter<ItemType, BaseItemViewHolder>(
    AsyncDifferConfig.Builder<ItemType>(callBack)
        .setBackgroundThreadExecutor(Executors.newSingleThreadExecutor())
        .build()
) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseItemViewHolder {

        return BaseItemViewHolder(

            DataBindingUtil.inflate(
                LayoutInflater.from(parent.context),
                getLayoutRes(viewType),
                parent, false
            )
        ).apply {
            onViewHolderCreated(this, viewType, binding)
        }

    }

    fun createCustomViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {

        return BaseItemViewHolder(

            DataBindingUtil.inflate(
                LayoutInflater.from(parent.context),
                getLayoutRes(viewType),
                parent, false
            )
        )
    }

    override fun onBindViewHolder(
        holder: BaseItemViewHolder,
        position: Int,
        payloads: MutableList<Any>
    ) {
        val item: ItemType? = currentList.getOrNull(position)

        item?.let {
            holder.binding.setVariable(BR.item, item)
            onViewHolderBound(holder.binding, item, position, payloads)
            holder.binding.executePendingBindings()
        }

    }

    override fun onBindViewHolder(holder: BaseItemViewHolder, position: Int) {

    }


    /**
     * get layout res based on view type
     */
    protected abstract fun getLayoutRes(viewType: Int): Int

    /**
     * Called when a ViewHolder is created. ViewHolder is either created first time or
     * when data is refreshed.
     *
     * This method is not called when RecyclerView is being scrolled
     */
    open fun onViewHolderCreated(
        viewHolder: RecyclerView.ViewHolder,
        viewType: Int,
        binding: ViewDataBinding
    ) {

        binding.root.setOnClickListener {
            onItemClicked?.invoke(getItem(viewHolder.bindingAdapterPosition), viewHolder.bindingAdapterPosition)
        }
    }

    /**
     * bind view while RecyclerView is being scrolled and new items are bound
     */
    open fun onViewHolderBound(
        binding: ViewDataBinding,
        item: ItemType,
        position: Int,
        payloads: MutableList<Any>
    ) {

    }


}

open class BaseItemViewHolder(
    val binding: ViewDataBinding
) : RecyclerView.ViewHolder(binding.root)


class DefaultItemDiffCallback<ItemType> : DiffUtil.ItemCallback<ItemType>() {

    override fun areItemsTheSame(
        oldItem: ItemType,
        newItem: ItemType
    ): Boolean {
        return oldItem === newItem
    }

    override fun areContentsTheSame(
        oldItem: ItemType,
        newItem: ItemType
    ): Boolean {
        return oldItem.hashCode() == newItem.hashCode()
    }
}

Another better user experience is using onBindViewHolder with payLoad which lets you only update some part of the rows instead of whole row. For instance you have image, title and body in rows, and only body changes frequently, without payload image flashes and provides bad user experience. But with payload you can decide which part of the row should be updated allowing you not to reload parts that were not updated.

Prurigo answered 15/8, 2020 at 11:33 Comment(3)
Answer is in java, I dont know why are you posting kotlin.Easel
@Easel Probably the author used Kotlin because it's the language the Android team recommends to use.Thickknee
@Thickknee not at the time when the question was asked.Easel
C
10

I found super duper easy method! I recommend this one

Example Code:

public class ContentAdapter extends RecyclerView.Adapter<ContentAdapter.ViewHolder> {

public interface OnItemClickListener {
    void onItemClick(ContentItem item);
}

private final List<ContentItem> items;
private final OnItemClickListener listener;

public ContentAdapter(List<ContentItem> items, OnItemClickListener listener) {
    this.items = items;
    this.listener = listener;
}

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

@Override public void onBindViewHolder(ViewHolder holder, int position) {
    holder.bind(items.get(position), listener);
}

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

static class ViewHolder extends RecyclerView.ViewHolder {

    private TextView name;
    private ImageView image;

    public ViewHolder(View itemView) {
        super(itemView);
        name = (TextView) itemView.findViewById(R.id.name);
        image = (ImageView) itemView.findViewById(R.id.image);
    }

    public void bind(final ContentItem item, final OnItemClickListener listener) {
        name.setText(item.name);
        Picasso.with(itemView.getContext()).load(item.imageUrl).into(image);
        itemView.setOnClickListener(new View.OnClickListener() {
            @Override public void onClick(View v) {
                listener.onItemClick(item);
            }
        });
    }
}
}

And Use RecyclerView Adapter using below code:

recycler.setAdapter(new ContentAdapter(items, new ContentAdapter.OnItemClickListener() {
@Override public void onItemClick(ContentItem item) {
    Toast.makeText(getContext(), "Item Clicked", Toast.LENGTH_LONG).show();
}
}));

i found this from here

Hope it helped you.

Cordite answered 12/8, 2018 at 20:29 Comment(0)
E
7

In my way, I just created a single instance of ClickListener, And it dispatches click event to both RecyclerView and Activity or Fragment:

class LeagueAdapter(
    onLeagueSelected: (League, Int, View) -> Unit
) : RecyclerView.Adapter<LeagueHolder>() {
    private val dataSet = arrayListOf<League>()

    private val clickListener = View.OnClickListener { view ->
        val adapterPosition = view.tag as Int
        onLeagueSelected(dataSet[adapterPosition], adapterPosition, view)

        // perform adapter related action here ...
    }


    override fun getItemCount(): Int {
        return dataSet.size
    }
    
    override fun onBindViewHolder(holder: LeagueHolder, position: Int) {
        // put item position in tag field
        holder.itemView.tag = position
        holder.itemView.setOnClickListener(clickListener)
    }
}

And inside Activity, we have something like this:

private val headerAdapter = LeagueAdapter { league, i, view ->
    Log.e(TAG, "item clicked $i")
}
Expecting answered 11/7, 2020 at 6:16 Comment(1)
Since onBindViewHolder is called multiple times then every time click listener is assigned which is unnecessary and can be avoided if you implement click listener inside ViewHolder instead of Adapter.Helminth
R
4

You can let your Activity implements View.OnClickListener and pass it to adapter. Below is an example.

class  RAdapter extends RecyclerView.Adapter<>{
    View.OnClickListener listner;
    public RAdapter(View.OnClickListener listner) {
        this.listner = listner;
    }
    public void onBindViewHolder(MyHolder holder, final int position) {
        holder.vname.setOnClickListener(listner);

    }
}

But to handle click in Activity you will going to need clicked position. You can have it with adapter.getAdapterPosition() to validate which item is clicked.

Apart from that To pass click event to the Fragment/Activity you can use a Custom callback listener this way your Adapter will be reusable .

A better way to handle clicks in ViewHolder. See the below example.

class Holder extends RecyclerView.ViewHolder implements View.OnClickListener {
        Button button;
        public Holder(View itemView) {
            super(itemView);
            button=itemView.findViewById(R.id.b1);
            button.setOnClickListener(this);
        }
        @Override
        public void onClick(View v) {
            if(v.getId()==R.id.b1){
                int position=getAdapterPosition();
                // Call the call method here 
                // with position or data Object itself
            }
        }
    }
Retributive answered 22/4, 2018 at 18:33 Comment(3)
An adapter connects a View (be it Fragment, Activity whatever) to a View in a RecyclerView (that's why it's called an adapter). The adapter itself shouldn't know anything about the calling View (in your example, the Activity)Barbarism
I know what a adapter is . What u suggest if one have to call a method of Activity from adapter?Retributive
What I'm saying is your comment // Call a public method of Activity here is very wrong and anti-pattern. If you want to communicate back to the caller you need a callback function (interfaces for example). Up to you how to design that.Barbarism
P
4

Create an interface for the adapter class

private OnItemClickListener mListener;

public CustomAdapter(List<Listdata> listdata, OnItemClickListener listener) {
    mListener = listener;
    ...
    ...
}

private class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {

    ViewHolder(View view) {
        ...
        ...
        view.setOnClickLister(this);
    }

    @override
    public void onClick(View v) {
        mListener.onAdapterItemClick(getAdapterPosition())
    }
}

interface OnItemClickListener {
    void onAdapterItemClick(int position);
}

Let the activity implement the interface

public class CustomListActivity extends AppCompatActivity implements OnItemClickListener {

...
...

@override
public void onAdapterItemClick(int position) {
    Toast.makeText(activity, "clicked on " +position, Toast.LENGTH_SHORT).show();
}

There is another way of doing this, check out this implementation

Poulenc answered 22/4, 2018 at 18:43 Comment(0)
R
3

If I understood correctly you want to set the on click logic in the Activity.

You can do this by setting the OnClickListener in the Activity and passing it in the Adapter constructor.

MyAdapter myAdapter = new MyAdapter(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            Toast.makeText(activity, "clicked on " +position, Toast.LENGTH_SHORT).show();
        }
    }));

And your MyAdapter Constructor would be:

final private OnClickListener onClickListener;

public MyAdapter(OnClickListener onClickListener) {
    this.OnClickListener = OnClickListener;
}

So your new code would be something like this

public void onBindViewHolder(MyHolder holder, final int position) {
    final Listdata data = listdata.get(position);
    holder.vname.setText(data.getName());

    holder.vname.setOnClickListener(onClickListener);

}

Recursion answered 22/4, 2018 at 18:42 Comment(0)
C
3

There's another very simple way documented in CodePath.

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

The implementation of ItemClickSupport.

Circumstantiate answered 20/7, 2021 at 11:36 Comment(2)
You have just saved me so much time!! I've been at this all day. Thank you.Wellfounded
Very nice solution, and reutilizable everywhere, thanks for linking itSeisin
V
2

RecyclerView widget only has 2 useful listeners for this scenario:

the code is inspired by TouchEvents sample related to Accessibility, and works in Activity/Fragment without setting any listeners in the Adapter

recyclerView.addOnItemTouchListener(object : RecyclerView.SimpleOnItemTouchListener() {
    var downTouch = false
    override fun onInterceptTouchEvent(rv: RecyclerView, e: MotionEvent): Boolean {
        when (e.action) {
            MotionEvent.ACTION_DOWN -> downTouch = true
            MotionEvent.ACTION_UP -> if (downTouch) {
                downTouch = false
                recyclerView.findChildViewUnder(e.x, e.y)?.let {
                    val position = rv.getChildAdapterPosition(it)
                    Toast.makeText(rv.context, "clicked on $position", Toast.LENGTH_SHORT)
                        .show()
                }
            }
            else -> downTouch = false
        }
        return super.onInterceptTouchEvent(rv, e)
    }
})
Veasey answered 20/8, 2020 at 22:19 Comment(0)
T
2

You can implement the View.OnClickListener interface in your RecyclerView.ViewHolder class and call it from there.

In your Adapter class create a public interface.

public void setOnItemClickListener(OnItemClickListener onItemClickListener) {
    this.onItemClickListener = onItemClickListener;
}

public interface OnItemClickListener {
    void onItemClick(int position);
}

private OnItemClickListener onItemClickListener;

On your ViewHolder class, you can implement the View.OnClickListener interface and set an onclick listener to the itemView.

public class MyViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener{
    public TextView myText;
    public WalletViewHolder(@NonNull View itemView) {
        super(itemView);
        myText= itemView.findViewById(R.id.my_text_view);

        // Set click listener for each item view
        itemView.setOnClickListener(this);
    }

    @Override
    public void onClick(View view) {
        if (onItemClickListener != null) {
            onItemClickListener.onItemClick(getAdapterPosition());
        }
    }
}

Then your OnItemClickListener will be created only once.

Tellurian answered 28/12, 2022 at 16:57 Comment(0)
S
1

Personally, I like to handle this via RxJava subjects:

A Subject is a sort of bridge or proxy that is available in some implementations of ReactiveX that acts both as an observer and as an Observable. Because it is an observer, it can subscribe to one or more Observables, and because it is an Observable, it can pass through the items it observes by re-emitting them, and it can also emit new items.

For more info read Understanding RxJava Subject — Publish, Replay, Behavior and Async Subject.

in Adapter:

public static PublishSubject<MyData> onClickSubject = PublishSubject.create();

ViewHolder:

public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
    .
    .
    .

    @Override
    public void onClick(View view) {
        onClickSubject.onNext(getItem(getAdapterPosition()));
    }
}

Add your disposables to a CompositeDisposable and dispose them in onDestroy():

private CompositeDisposable compositeDisposable = new CompositeDisposable();

in onCreate():

compositeDisposable.add(MyAdapter.onClickSubject.subscribe(myData -> {
    //do something here
}));

in onDestroy():

compositeDisposable.dispose();

Note:

1. getItem() is a method of androidx.recyclerview.widget.ListAdapter and androidx.paging.PagedListAdapter if you are extending RecyclerView.Adapter you can get item from your data list by position.

2. to use Disposables you need RxJava2 or above

Streamlet answered 15/8, 2020 at 10:57 Comment(0)
H
1

Kotlin

I'd better to add item click in onCreateViewHolder like this

override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): 
ProductViewHolder {
    val view: View = LayoutInflater.from(viewGroup.context)
        .inflate(R.layout.layout_product_item, viewGroup, false)
    return ProductViewHolder(view).also { viewHolder ->
        viewHolder.itemView.setOnClickListener {
            val position = viewHolder.layoutPosition
            if (position != RecyclerView.NO_POSITION) {
                // do what you want with data[position]
            }
        }
    }
}
Hengel answered 9/12, 2022 at 21:47 Comment(0)
S
-1

I always have one Generic Adapter in my project to avoid make a Adapter class every I use a Recyclerview. Here some example

public class AdapterRecyclerviewTextOnly extends RecyclerView.Adapter<AdapterRecyclerviewTextOnly.ViewHolder> {
private RecyclerView recyclerView;
private OnRecyclerviewListener onRecyclerviewListener;

public interface OnRecyclerviewListener {
    void onRecyclerviewBind(RecyclerView recyclerView, AdapterRecyclerviewTextOnly.ViewHolder viewHolder, int position);
    void onRecyclerviewClick(RecyclerView recyclerView, int position);
    int onItemCount(RecyclerView recyclerView);
}

public void setOnRecyclerviewListener(OnRecyclerviewListener listener) { this.onRecyclerviewListener = listener; }

public AdapterRecyclerviewTextOnly(RecyclerView recyclerView) {
    super();
    this.recyclerView = recyclerView;
}


public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
    RecyclerView recyclerView;
    public TextView textView;

    ViewHolder(RecyclerView recyclerView, View itemView) {
        super(itemView);
        this.recyclerView = recyclerView;
        this.itemView.setOnClickListener(this);

        this.textView = itemView.findViewById(R.id.textview_title);
    }

    void onBind(int position) { onRecyclerviewListener.onRecyclerviewBind(this.recyclerView, this, position); }

    @Override
    public void onClick(View v) {
        onRecyclerviewListener.onRecyclerviewClick(this.recyclerView, getAdapterPosition());
    }
}

@Override
public AdapterRecyclerviewTextOnly.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    View inflatedView = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_recyclerview_text_only, parent, false);
    return new ViewHolder(this.recyclerView, inflatedView);
}

@Override
public void onBindViewHolder(AdapterRecyclerviewTextOnly.ViewHolder holder, int position) {
    holder.onBind(position);
}

@Override
public int getItemCount() {
    return onRecyclerviewListener.onItemCount(this.recyclerView);
}
}

And then in your Activity Class, you can use this adapter with :

    this.recyclerView = findViewById(R.id.recyclerview);
    this.recyclerView.setHasFixedSize(true);
    this.recyclerView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false));
    AdapterRecyclerviewTextOnly recyclerViewAdapter = new AdapterRecyclerviewTextOnly(this.recyclerView);
    this.recyclerView.setAdapter(this.recyclerViewAdapter);
    this.recyclerViewAdapter.setOnRecyclerviewListener(new AdapterRecyclerviewTextOnly.OnRecyclerviewListener() {
        @Override
        public void onRecyclerviewBind(RecyclerView recyclerView, AdapterRecyclerviewTextOnly.ViewHolder viewHolder, int position) {

        }

        @Override
        public void onRecyclerviewClick(RecyclerView recyclerView, int position) {

        }

        @Override
        public int onItemCount(RecyclerView recyclerView) { 
}
});

You can reuse this with 2 or 3 recyclerview too. First, declare a globar listener private AdapterRecyclerviewTextOnly.OnRecyclerviewListener listener;.

Then init the listener with new object then set the your every recyclerview with the listener. Use specific identifier:

if (recyclerView == recyclerViewA){ } else if (recyclerView == recyclerViewB) { } to manage your recyclerview inside the adapter.

Sauls answered 11/3, 2019 at 10:25 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.