RecyclerView Item Click Listener the Right Way

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.


very simple and clean solution is:

  create a class with the name of RecyclerTouchListener:

    public class RecyclerTouchListener implements RecyclerView.OnItemTouchListener {

        private GestureDetector gestureDetector;
        private ClickListener clickListener;

        public RecyclerTouchListener(Context context, final RecyclerView recyclerView, final ClickListener clickListener) {
            this.clickListener = clickListener;
            gestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
                @Override
                public boolean onSingleTapUp(MotionEvent e) {
                    return true;
                }

                @Override
                public void onLongPress(MotionEvent e) {
                    View child = recyclerView.findChildViewUnder(e.getX(), e.getY());
                    if (child != null && clickListener != null) {
                        clickListener.onLongClick(child, recyclerView.getChildPosition(child));
                    }
                }
            });
        }

        @Override
        public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {

            View child = rv.findChildViewUnder(e.getX(), e.getY());
            if (child != null && clickListener != null && gestureDetector.onTouchEvent(e)) {
                clickListener.onClick(child, rv.getChildPosition(child));
            }
            return false;
        }

        @Override
        public void onTouchEvent(RecyclerView rv, MotionEvent e) {
        }

        @Override
        public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {

        }

        public interface ClickListener {
            void onClick(View view, int position);

            void onLongClick(View view, int position);
        }
    }

in your recyclerview activity:

recyclerView.addOnItemTouchListener(new RecyclerTouchListener(getApplicationContext(), recyclerView, new RecyclerTouchListener.ClickListener() {
            @Override
            public void onClick(View view, int position) {
                speech(countries_list_code[position]);
            }

            @Override
            public void onLongClick(View view, int position) {

            }
        }));

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.


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