Android
RecyclerView

Not bad RecyclerView Practices 1: OnItemClickListener

More than 3 years have passed since last update.


0. tl,dr:

I created a repo: RVP

Where the most important classes are stored in core:

BaseAdapter.java - will generally support basic RecyclerView#Adapter's behavior. It comes with a RecyclerView's version of OnItemClickListener (I was inspired by AdapterView's interface).

BaseListAdapter.java - an extend of BaseAdapter, focuses on List of a specific Data type.

Some examples are included outside, take a look at package fragment and adapter.


1. So we Click Item, but we don't know the good way to listen to that Event



  • Problem and My Opinions:


    • Since the release of RecyclerView and its childs (Other libraries based on RecyclerView), there are various ways to interaction to item, and there are event more ways to listen to those event. -- My Opinion: The need of a On Item Click Listener thing is obvious, but there's not been a globally good practice (which is good for all cases) already.

    • Many Developers keep in mind that RecyclerView is a Updated version of ListView and GridView, so they expect some similar behavior of those Views on RecyclerView. -- My Opinion: this thought is not always true. RecyclerView is a superset of a general AdapterView - the View which requires Adapter to populate its UI. RecyclerView can do a lot more than the legacy AdapterView. Further more, current implementations of OnItemClickListener are trying to mimic the famous AdapterView#setOnItemClickListener() setup, which is generally not that bad, but we should not rely on or try to reproduce it by all means.



  • My Solution: in short, instead of trying to mimic this setup AdapterView#setOnItemClickListener(), I use RecyclerView$Adapter instead, which will become something like this: MyAdapter#setOnItemClickListener().



2. Base classes

talk is cheap, so me some codes


BaseAdapter.class

/**

* A less abstract Adapter, to simplify RecyclerView#Adapter implementation
*/

public abstract class BaseAdapter<VH extends BaseAdapter.ViewHolder>
extends RecyclerView.Adapter<VH> {

/**
* This custom onClick event listener should be set by the Adapter
*/

protected OnItemClickListener mOnItemClickListener;

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

/**
* An abstract ViewHolder
*/

public abstract static class ViewHolder extends RecyclerView.ViewHolder {

public ViewHolder(@NonNull View itemView) {
super(itemView);
}

/**
* This method is supposed to be override every time. For example, a view holder holds more
* than 2 views, and Client want to listen to the click event on each of them.
* <p/>
* By default, the main #itemView will receive the click event.
* <p/>
* !IMPORTANT: This method is used optionally.
*
* @param listener to listen to Click event
*/

// NOTE: Long name, I know
public void setOnViewHolderClickListener(View.OnClickListener listener) {
itemView.setOnClickListener(listener);
}
}

public interface OnItemClickListener {

/**
* Interact with RecyclerView's item on click event
*
* @param adapter who holds data
* @param viewHolder the #ViewHolder who receives click event
* @param view the view under the click event
* @param adapterPosition position of clicked item in adapter
* @param itemId retrieve from
* {@link android.support.v7.widget.RecyclerView.Adapter#getItemId(int)}
*/

void onItemClick(BaseAdapter adapter, ViewHolder viewHolder, View view,
int adapterPosition, long itemId);
}
}


Just a simple extends of RecyclerView$Adapter, where I define a kind of OnItemClickListener interface with the main character: onItemClick method. This method takes the Adapter as a neccessary member, where we could retrieve data and more. !NOTE: Replacing Adapter by RecyclerView it self is Ok, since we could get Adapter from it. But keep in mind that a RecyclerView holds a lot more memory than an Adapter.

Here we also have a simple implementation of ViewHolder, which supports seting the interaction event setup. The default setup will be the Click event on ViewHolder#itemView, but Client could (and should) override this to provide more expected behaviors (Take a look at my Sample classes).

This Based setup will struct a vision for Client to implement it own Click listener on Children classes.


BaseListAdapter.class

/**

* @param <T> This Adapter is specified to use with List of Objects of type T
*/

public abstract class BaseListAdapter<T> extends BaseAdapter<BaseListAdapter.ViewHolder<T>> {

/**
* This Adapter must support retrieving item(s)
* <p/>
* !IMPORTANT General Adapter could support various Types of Object, so we must not force it to
* return a single Type of object. This BaseListAdapter was created to support those cases.
*
* @param position of the item we want to get
* @return expected Item at a position
*/

public abstract T getItem(int position);

/**
* If Client implement this method, He must call super.onBindViewHolder for expected
* behaviors.
*
* @param holder
* @param position
*/

@CallSuper
@Override public void onBindViewHolder(ViewHolder<T> holder, int position) {
T item = getItem(position);
if (item != null) {
holder.bindInternal(item);
}
}

/**
* For now we don't support this method.
*
* @param holder
* @param position
* @param payloads
*/

/*hide*/
@Override
public void onBindViewHolder(ViewHolder<T> holder, int position, List<Object> payloads) {
super.onBindViewHolder(holder, position, payloads);
}

/**
* General abstract ViewHolder to support specific Data type
*
* @param <T> expected Data Type
*/

public abstract static class ViewHolder<T> extends BaseAdapter.ViewHolder {

// I think it's not bad to have an shallow copy of current Data
protected T mItem;

public ViewHolder(@NonNull View itemView) {
super(itemView);
}

// This method will always be called by Adapter
void bindInternal(T item) {
mItem = item;
bind(item);
}

// Client then update its ViewHolder's appearance here
public abstract void bind(T item);
}
}


This class is an simple extension of the BaseAdapter above, in respect to android.widget.ArrayAdapter. BaseListAdapter suppose to support a specific Data Type T, come along with a special ViewHolder class to ensure that each ViewHolder will represent an Object of type T. ViewHolder#bindInternal are called to make sure that a ViewHolder instance hold its mItem for later use. This implementation is Optional.

2 Base Classes above are how I prepare the base for any further interaction implementation. In fact, the same implementation could be done to support OnItemLongClickListener.


3. OnItemClick in action

I prepared 2 Fragment, who hold 2 lists: a Simple list of String and a more complicated list of Cheese (borrow the idea, some pieces of code and resources from Cheesesquare).

Let's start from something complicated :D.


  • CheeseListFragment and CheeseListAdapter:

An implementation of ViewHolder for a Cheese list:


CheeseListAdapter$ViewHolder.class

public static class ViewHolder extends BaseListAdapter.ViewHolder<Cheeses> {

// A ViewHolder expects a UI Layout, so make it static here
static final int LAYOUT_RES = R.layout.list_item;

public final ImageView mImageView;
public final TextView mTextView;

public ViewHolder(View view) {
super(view);
mImageView = (ImageView) view.findViewById(R.id.item_icon);
mTextView = (TextView) view.findViewById(R.id.item_name);
}

@Override public void bind(Cheeses item) {
mTextView.setText(item.getName());
Picasso.with(itemView.getContext())
.load(item.getIconRes())
.fit().centerCrop()
.into(mImageView);
}

// !IMPORTANT Since we accept click event on 2 different views, we must delegate them here.
@Override public void setOnViewHolderClickListener(View.OnClickListener listener) {
mImageView.setOnClickListener(listener);
itemView.setOnClickListener(listener);
}
}


This ViewHolder is exactly the same as Cheesesquare's list view holder. But it provides better interaction: click on Cheese's image and click on the item's cell. To support that, we simply override #setOnViewHolderClickListener and setup the normal OnClick event for whatever Views from which we want to listen to click event. Here I setup for my mImageView and the itemView (which holds mImageView as its child).


CheeseListAdapter.class

  // Just because I like this number

private static final int LIST_SIZE = 23;

private static final List<Cheeses> sItems = getRandomSublist(Cheeses.sCheeseStrings, LIST_SIZE);

@Override public Cheeses getItem(int position) {
return sItems.get(position);
}

@Override
public BaseListAdapter.ViewHolder<Cheeses> onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(ViewHolder.LAYOUT_RES, parent, false);
final ViewHolder viewHolder = new ViewHolder(view);
// setup Click event listener here
viewHolder.setOnViewHolderClickListener(new View.OnClickListener() {
@Override public void onClick(View v) {
if (CheeseListAdapter.this.mOnItemClickListener != null) {
int adapterPosition = viewHolder.getAdapterPosition();
// get this from Android Summit Video
if (adapterPosition != RecyclerView.NO_POSITION) {
CheeseListAdapter.this.mOnItemClickListener.onItemClick(
CheeseListAdapter.this, viewHolder, v, adapterPosition, getItemId(adapterPosition)
);
}
}
}
});
return viewHolder;
}

/**
* Customized Click event listener
*/

public abstract static class OnCheeseClickListener implements OnItemClickListener {

public abstract void onIconClick(View iconView, Cheeses cheese);

public abstract void onCheeseClick(View nameView, Cheeses cheese);

@Override
public void onItemClick(BaseAdapter adapter, BaseAdapter.ViewHolder viewHolder,
View view, int adapterPosition, long itemId) {
if (adapter instanceof BaseListAdapter) {
// we expect a ListAdapter here, since we are using a List Adapter
BaseListAdapter listAdapter = (BaseListAdapter) adapter;
// so we can get clicked item
Cheeses item = (Cheeses) listAdapter.getItem(adapterPosition);

// Note 1: Casted object maybe null if we're using wrong adapter
// Note 2:
if (item != null && viewHolder instanceof ViewHolder) {
if (view == ((ViewHolder) viewHolder).mImageView) {
onIconClick(view, item);
} else if (view == ((ViewHolder) viewHolder).itemView) {
onCheeseClick(view, item);
}
}
}
}
}

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

List<Cheeses> getRandomSublist(String[] array, int amount) {
ArrayList<Cheeses> list = new ArrayList<>(amount);
... // refer Cheesesquare
return list;
}

public static class ViewHolder extends BaseListAdapter.ViewHolder<Cheeses> {
... // above
}
}


So for my custom click listener behavior, I have OnCheeseClickListener which implement BaseAdapter$OnItemClickListener. By that, I could provide custom behavior for each components in each of this list's ViewHolder objects.

CheeseListAdapter#onCreateViewHolder is where we pass the Click event listener from parent Adapter to its ViewHolder. Inspired from Android Summit 2015 talks, that is where we should do that works, and ViewHolder#getAdapterPosition() is the position we should use to get Data.

Now, take a look at CheeseListFragment


CheeseListFragment.class

public class CheeseListFragment extends Fragment {

RecyclerView mRecyclerView;
RecyclerView.LayoutManager mLayoutManager;
CheeseListAdapter mAdapter;
/**
* My customized click listener. We can directly handle click event from Fragment or Setup
* another callback to attach to Host Activity's lifecycle. IMO, Both are good practices.
*/

private CheeseListAdapter.OnCheeseClickListener mClickListener;

public CheeseListFragment() {
}

public static CheeseListFragment newInstance() {
return new CheeseListFragment();
}

@Nullable @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle
savedInstanceState) {
return inflater.inflate(R.layout.fragment_cheese_list, container, false);
}

@Override public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mRecyclerView = (RecyclerView) view.findViewById(R.id.recycler_view);
/*
We can try both UI to check if our position works as expected;
*/

// mLayoutManager = new LinearLayoutManager(getContext(), LinearLayoutManager.VERTICAL, false);
mLayoutManager = new GridLayoutManager(getContext(), 2, LinearLayoutManager.VERTICAL, false);
// a little bit more complicated Grid.
GridLayoutManager.SpanSizeLookup spanSizeLookup = new GridLayoutManager.SpanSizeLookup() {
@Override public int getSpanSize(int position) {
return position % 3 == 2 ? 2 : 1;
}
};

((GridLayoutManager) mLayoutManager).setSpanSizeLookup(spanSizeLookup);

mRecyclerView.setLayoutManager(mLayoutManager);

mAdapter = new CheeseListAdapter();
mRecyclerView.setAdapter(mAdapter);

mClickListener = new CheeseListAdapter.OnCheeseClickListener() {
@Override public void onIconClick(View iconView, Cheeses cheese) {
if (getView() != null) {
Snackbar.make(getView(), "Icon clicked: " + cheese.getName(), Snackbar.LENGTH_LONG)
.show();
}
}

@Override public void onCheeseClick(View nameView, Cheeses cheese) {
Intent intent = new Intent(getContext(), CheeseDetailActivity.class);
intent.putExtra(CheeseDetailActivity.EXTRA_NAME, cheese.getName());
startActivity(intent);
}
};

mAdapter.setOnItemClickListener(mClickListener);
}

@Override public void onDestroyView() {
// This click event is attached to UI behavior, so we should properly release it before all
// views are dead.
// IMO, Doing this in onDestroyView or onDetach is really depends on how you use your listener.
mClickListener = null;
super.onDestroyView();
}
}


There's been various practices on how to pass logic to Fragment from Activity. I don't discuss it here. The point here is how we pass that event to our Adapter, which is simply like this:

CheeseListAdapter.OnCheeseClickListener  mClickListener = new CheeseListAdapter.OnCheeseClickListener() {

@Override public void onIconClick(View iconView, Cheeses cheese) {
if (getView() != null) {
Snackbar.make(getView(), "Icon clicked: " + cheese.getName(), Snackbar.LENGTH_LONG)
.show();
}
}

@Override public void onCheeseClick(View nameView, Cheeses cheese) {
Intent intent = new Intent(getContext(), CheeseDetailActivity.class);
intent.putExtra(CheeseDetailActivity.EXTRA_NAME, cheese.getName());
startActivity(intent);
}
};

mAdapter.setOnItemClickListener(mClickListener);

For the general Activity-Fragment-Callback implementation, we can have an interface from our Fragment, and retrieve its instance by onAttach, I leave it for Client. But it would not be complicated.

Above example looks complicated, now we look at a much more simpler Example: A simple String list

```Java: SimpleListAdapter$ViewHolder.class

public static class ViewHolder extends BaseListAdapter.ViewHolder {

// This is a TextView's layout

static final int LAYOUT_RES = android.R.layout.simple_list_item_1;

public ViewHolder(@NonNull View itemView) {
super(itemView);
}

@Override public void bind(String item) {
// It's safe here
((TextView) itemView).setText(item);
}

}

```

Comparing to Cheese list's Viewholder, we have simpler class here, without any setup for On Click event. Because by default, ViewHolder#itemView will handle that event if set (BaseAdapter$ViewHolder#setOnViewHolderClickListener()).


SimpleListAdapter.class

public class SimpleListAdapter extends BaseListAdapter<String> {

List<String> getRandomSublist(String[] array, int amount) {
ArrayList<String> list = new ArrayList<>(amount);
... // refer Cheesesquare
return list;
}

// Just because I like this number
private static final int LIST_SIZE = 23;

private final List<String> sItems = getRandomSublist(Cheeses.sCheeseStrings, LIST_SIZE);

@Override public String getItem(int position) {
return sItems.get(position);
}

@Override
public BaseListAdapter.ViewHolder<String> onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(ViewHolder.LAYOUT_RES, parent, false);
final ViewHolder viewHolder = new ViewHolder(view);
viewHolder.setOnViewHolderClickListener(new View.OnClickListener() {
@Override public void onClick(View v) {
int adapterPosition = viewHolder.getAdapterPosition();
// get this from Android Summit Video
if (adapterPosition != RecyclerView.NO_POSITION) {
SimpleListAdapter.this.mOnItemClickListener.onItemClick(
SimpleListAdapter.this, viewHolder, v, adapterPosition, getItemId(adapterPosition)
);
}
}
});

return viewHolder;
}

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

public static class ViewHolder extends BaseListAdapter.ViewHolder<String> {
... // above
}
}


So except for the custom OnCheeseClickListener, this class is the same as CheeseListAdapter, with a little mount of code. The most important part was still the #onCreateViewHolder implementation.


SimpleListFragment.class

public class SimpleListFragment extends Fragment {

public SimpleListFragment() {
}

public static SimpleListFragment newInstance() {
return new SimpleListFragment();
}

RecyclerView mRecyclerView;
RecyclerView.LayoutManager mLayoutManager;
SimpleListAdapter mAdapter;
BaseAdapter.OnItemClickListener mClickListener;

@Nullable @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle
savedInstanceState) {
return inflater.inflate(R.layout.fragment_cheese_list, container, false);
}

@Override public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mRecyclerView = (RecyclerView) view.findViewById(R.id.recycler_view);
mLayoutManager = new LinearLayoutManager(getContext(), LinearLayoutManager.VERTICAL, false);
mRecyclerView.setLayoutManager(mLayoutManager);

mAdapter = new SimpleListAdapter();
mRecyclerView.setAdapter(mAdapter);

mClickListener = new BaseAdapter.OnItemClickListener() {
@Override
public void onItemClick(BaseAdapter adapter, BaseAdapter.ViewHolder viewHolder, View view,
int adapterPosition, long itemId) {
String item = null;
if (adapter instanceof SimpleListAdapter) {
item = ((SimpleListAdapter) adapter).getItem(adapterPosition);
}

if (item != null) {
Intent intent = new Intent(getContext(), CheeseDetailActivity.class);
intent.putExtra(CheeseDetailActivity.EXTRA_NAME, item);
startActivity(intent);
}
}
};

mAdapter.setOnItemClickListener(mClickListener);
}

@Override public void onDestroyView() {
// This click event is attached to UI behavior, so we should properly release it before all
// views are dead.
// IMO, Doing this in onDestroyView or onDetach is really depends on how you use your listener.
mClickListener = null;
super.onDestroyView();
}
}


Here we have almost the same structure with CheeseListFragment. And again this is not a bunch of code in my opinion :trollface:.


4. Conclusion and Future works

This is neither a new implementation nor a new Idea. It's just another way to handle the famous click event, created specially for RecyclerView. This is based on one point from Android Summit 2015 talk, where RecyclerView's creator pointed out where should be the place we setup the click event listener. The rest of this project tries to support our common sense of setting up our legacy AdapterView. But it doesn't try to attach on AdapterView point of view, but it goes by the think of RecyclerView: Adapter is where we start everything, RecyclerView will support the hard works. So by this point of view, I hope this implementation could be considered a not bad practices.

More works need to be done for more interaction event (long click, for example). And there are more rooms for others NOT BAD RECYCLERVIEW PRACTICES too (At least, I was talking about OnClickListener this time only). So hopefully more practices are coming.


5. Last but not least

Re: I created a repo: RVP

This project is written in plain MVC, so it might not look good or even useful in other design patterns though. So use it at your own risk, and any contribution are welcome.

This project follows Google's Java coding style (tl,dr: 2-spaces-indent) :D.