Leonid Ustenko
Aug 24, 2017 · 7 min read

Generic RecyclerView adapter

If you develop quite a complex Android application, you might face a problem with a tremendous number of RecyclerView adapters for different screens which showing different item types. This article will show you one of the possible ways of reducing the number of boilerplate code when dealing with the adapters by making a generic one and extending it for a particular use case.

By the end of this article we will be able to create as simple adapters as this:

public class UsersAdapter extends GenericRecyclerAdapter<User, OnRecyclerObjectClickListener<User>, UserViewHolder> {

public UsersAdapter(Context context) {
super(context);
}

@Override
public UserViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new UserViewHolder(inflate(R.layout.item_user, parent));
}
}

Having looked at different RecyclerView adapters you may notice that as a usual, they perform some common basics.

Most of the Recycler adapters:

- are been populated/depopulated with items of some type

- create ViewHolders

- bind ViewHolders

- set listeners

Most of the ViewHolders:

- hold references onto Views

- bind data to the views

- set listeners

Most listeners:

- are used to retrieve the item which has been clicked (object associated with it).

Having summarized that, we can set off for making all the common things generic.

To begin with, let’s make the items generic:

private List<T> items;

We initialize them in the constructor and create the first method which lets us populate items:

/**
* Sets items to the adapter and notifies that data set has been changed.
*
*
@param items items to set to the adapter
*
@throws IllegalArgumentException in case of setting `null` items
*/
public void setItems(List<T> items) {
if (items == null) {
throw new IllegalArgumentException("Cannot set `null` item to the Recycler adapter");
}
this.items.clear();
this.items.addAll(items);
notifyDataSetChanged();
}

Other methods such as adding/removing/clearing will be added later to keep things simple.

The next step is to create a listener which will be handy to use.

Firstly create a base listener contract:

public interface BaseRecyclerListener {
}

Then make its implementation.

Here we implement a listener which will provide us with the reference of the object, associated with the clicked item.

Generic implementation will look like:

public interface OnRecyclerObjectClickListener<T> extends BaseRecyclerListener {
/**
* Item has been clicked.
*
*
@param item object associated with the clicked item.
*/
void onItemClicked(T item);
}

Id or any other parameter (if you wish so) also can easily be retrieved witj another listener:

public interface OnRecyclerClickListener extends BaseRecyclerListener {
/**
* RecyclerView item has been clicked
*
*
@param id item id
*/
void onItemClicked(long id);
}

The next step is to add the listener to our generic adapter.

private L listener;
/**
* Set click listener, which must extend {
@link BaseRecyclerListener}
*
*
@param listener click listener
*/
public void setListener(L listener) {
this.listener = listener;
}

Now is the last stepping stone — creating of the generic ViewHolder.

public abstract class BaseViewHolder<T, L extends BaseRecyclerListener> extends RecyclerView.ViewHolder {

public BaseViewHolder(View itemView) {
super(itemView);
}

/**
* Bind data to the item and set listener if needed.
*
*
@param item object, associated with the item.
*
@param listener listener a listener {@link BaseRecyclerListener} which has to b set at the item (if not `null`).
*/
public abstract void onBind(T item, @Nullable L listener);
}

Note that <T> is the type of object associated with a view holder item (revived previously) and <L> is the defined previously listener.

Here we go!

Our generic adapter is almost ready.

It looks now in a next way (after adding a couple of helper methods):

/**
* Base generic RecyclerView adapter.
* Handles basic logic such as adding/removing items,
* setting listener, binding ViewHolders.
* Extend the adapter for appropriate use case.
*
*
@param <T> type of objects, which will be used in the adapter's dataset
*
@param <L> click listener {@link BaseRecyclerListener}
*
@param <VH> ViewHolder {@link BaseViewHolder}
*/
public abstract class GenericRecyclerAdapter<T, L extends BaseRecyclerListener, VH extends BaseViewHolder<T, L>> extends RecyclerView.Adapter<VH> {

private List<T> items;
private L listener;
private LayoutInflater layoutInflater;

/**
* Base constructor.
* Allocate adapter-related objects here if needed.
*
*
@param context Context needed to retrieve LayoutInflater
*/
public GenericRecyclerAdapter(Context context) {
layoutInflater = LayoutInflater.from(context);
items = new ArrayList<>();
}

/**
* To be implemented in as specific adapter
*
*
@param parent The ViewGroup into which the new View will be added after it is bound to an adapter position.
*
@param viewType The view type of the new View.
*
@return A new ViewHolder that holds a View of the given view type.
*/
@Override
public abstract VH onCreateViewHolder(ViewGroup parent, int viewType);

/**
* Called by RecyclerView to display the data at the specified position. This method should
* update the contents of the itemView to reflect the item at the given
* position.
*
*
@param holder The ViewHolder which should be updated to represent the contents of the
* item at the given position in the data set.
*
@param position The position of the item within the adapter's data set.
*/
@Override
public void onBindViewHolder(VH holder, int position) {
T item = items.get(position);
holder.onBind(item, listener);
}

/**
* Returns the total number of items in the data set held by the adapter.
*
*
@return The total number of items in this adapter.
*/
@Override
public int getItemCount() {
return items != null ? items.size() : 0;
}

/**
* Sets items to the adapter and notifies that data set has been changed.
*
*
@param items items to set to the adapter
*
@throws IllegalArgumentException in case of setting `null` items
*/
public void setItems(List<T> items) {
if (items == null) {
throw new IllegalArgumentException("Cannot set `null` item to the Recycler adapter");
}
this.items.clear();
this.items.addAll(items);
notifyDataSetChanged();
}

/**
* Returns all items from the data set held by the adapter.
*
*
@return All of items in this adapter.
*/
public List<T> getItems() {
return items;
}

/**
* Returns an items from the data set at a certain position.
*
*
@return All of items in this adapter.
*/
public T getItem(int position) {
return items.get(position);
}

/**
* Adds item to the end of the data set.
* Notifies that item has been inserted.
*
*
@param item item which has to be added to the adapter.
*/
public void add(T item) {
if (item == null) {
throw new IllegalArgumentException("Cannot add null item to the Recycler adapter");
}
items.add(item);
notifyItemInserted(items.size() - 1);
}

/**
* Adds list of items to the end of the adapter's data set.
* Notifies that item has been inserted.
*
*
@param items items which has to be added to the adapter.
*/
public void addAll(List<T> items) {
if (items == null) {
throw new IllegalArgumentException("Cannot add `null` items to the Recycler adapter");
}
this.items.addAll(items);
notifyItemRangeInserted(this.items.size() - items.size(), items.size());
}

/**
* Clears all the items in the adapter.
*/
public void clear() {
items.clear();
notifyDataSetChanged();
}

/**
* Removes an item from the adapter.
* Notifies that item has been removed.
*
*
@param item to be removed
*/
public void remove(T item) {
int position = items.indexOf(item);
if (position > -1) {
items.remove(position);
notifyItemRemoved(position);
}
}

/**
* Returns whether adapter is empty or not.
*
*
@return `true` if adapter is empty or `false` otherwise
*/
public boolean isEmpty() {
return getItemCount() == 0;
}

/**
* Indicates whether each item in the data set can be represented with a unique identifier
* of type {
@link Long}.
*
*
@param hasStableIds Whether items in data set have unique identifiers or not.
*
@see #hasStableIds()
*
@see #getItemId(int)
*/
@Override
public void setHasStableIds(boolean hasStableIds) {
super.setHasStableIds(hasStableIds);
}


/**
* Set click listener, which must extend {
@link BaseRecyclerListener}
*
*
@param listener click listener
*/
public void setListener(L listener) {
this.listener = listener;
}

/**
* Inflates a view.
*
*
@param layout layout to me inflater
*
@param parent container where to inflate
*
@param attachToRoot whether to attach to root or not
*
@return inflated View
*/
@NonNull
protected View inflate(@LayoutRes final int layout, @Nullable final ViewGroup parent, final boolean attachToRoot) {
return layoutInflater.inflate(layout, parent, attachToRoot);
}

/**
* Inflates a view.
*
*
@param layout layout to me inflater
*
@param parent container where to inflate
*
@return inflated View
*/
@NonNull
protected View inflate(@LayoutRes final int layout, final @Nullable ViewGroup parent) {
return inflate(layout, parent, false);
}
}

Last but not least is to make use of the adapter.

For the implementation we need only 2 things now:

1. Extend the generic adapter (you will be surprised by its size wouldn’t you?)

2. Create a ViewHolder.

Let’s consider having a list of User objects which we have to show in the recycler.

Proceed the mentioned above two steps:

1. Create adapter implementation which now looks like:

public class UsersAdapter extends GenericRecyclerAdapter<User, OnRecyclerObjectClickListener<User>, UserViewHolder> {

/**
* Base constructor.
* Allocate adapter-related objects here if needed.
*
*
@param context Context needed to retrieve LayoutInflater
*/
public UsersAdapter(Context context) {
super(context);
}

/**
* To be implemented in as specific adapter
*
*
@param parent The ViewGroup into which the new View will be added after it is bound to an adapter position.
*
@param viewType The view type of the new View.
*
@return A new ViewHolder that holds a View of the given view type.
*/
@Override
public UserViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new UserViewHolder(inflate(R.layout.item_user, parent));
}
}

2. Create a ViewHolder:

public class UserViewHolder extends BaseViewHolder<User, OnRecyclerObjectClickListener<User>> {

private TextView name;
private ImageView avatar;

public UserViewHolder(View itemView) {
super(itemView);
name = itemView.findViewById(R.id.tv_name);
avatar = itemView.findViewById(R.id.iv_avatar);
}

/**
* Bind data to the item and set listener if needed.
*
*
@param item object, associated with the item.
*
@param listener listener a listener {@link BaseRecyclerListener} which has to b set at the item (if not `null`).
*/
@Override
public void onBind(User item, @Nullable OnRecyclerObjectClickListener<User> listener) {
// set all data
name.setText(item.getName());
Glide.with(itemView.getContext())
.load(item.getImageUrl())
.centerCrop()
.crossFade()
.into(avatar);
// set listener if needed
// you can set it at any of the views instead of whole itemView if you wish
if (listener != null) {
itemView.setOnClickListener(v -> listener.onItemClicked(item));
}
}
}

And that’s it!

Just use it with a RecyclerView.

UsersAdapter adapter = new UsersAdapter(getActivity());
adapter.setListener(this);
recycler.setAdapter(adapter);
recycler.setLayoutManager(new LinearLayoutManager(getActivity()));

Populate the adapter when you have the items available:

adapter.setItems(users);

Note, that your Fragment or Activity now has to implement the listener: OnRecyclerObjectClickListener<User>

Where in the onItemClicked()

you will receive the clicked User object.

The more different adapters you need to implement, the more advantages you will see.

P.S. The functionality of an adapter can easily be extended.

For instance, for supporting several view types just override the getItemViewType()

method in your adapter implementation and return corresponding ViewHolder in the onCreateViewHolder()

Complete code and a sample application can be found here:

)
Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade