Adding Click Listeners to Views in Adapters

Despite using Adapters to achieve complex results on a nearly daily basis, it didn’t really hit home with me just how complex and powerful they really are until I attended an “Intro to Adapters” presentation at GDG-A3 this past week.  Joe Blough did a nice job showing how to get started with Adapters, you can get his slides and code if you wish to take a look.  One particular point started a bit of discussion, and that was dealing with click listeners attached to views returned by the Adapter. Below are three approaches to solving this problem that came from that discussion. I’m sure there are others, and the solution that works for you will depend on your use case.

So, for simplicity’s sake, let’s consider a simple ListView with an Adapter that has one type of View.  This view is pretty simple, containing just a TextView and a Button:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" >

    <TextView
        android:id="@+id/text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true" />

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:text="@string/Click_Me" />

</RelativeLayout>

Now, in your Adapter, you will want to hook up a click listener to the button and perform an action when the user clicks. More than likely, what happens when the user clicks the button is going to depend upon which button is clicked. For example, perhaps the “Click Me” button has the always useful task of informing the user of the index of the row in which the clicked button resides.

Simple (Anti-)Pattern

A simple solution might result in a getView() method that looks something like this:

@Override
public View getView(final int position, View convertView, ViewGroup parent) {
    ViewHolder holder;
    if (convertView == null) {
        convertView = LayoutInflater.from(getContext()).inflate(R.layout.row_simple, parent, false);
        holder = new ViewHolder();
        holder.text = (TextView) convertView.findViewById(R.id.text);
        holder.button = (Button) convertView.findViewById(R.id.button);
        convertView.setTag(holder);
    }
    else {
        holder = (ViewHolder) convertView.getTag();
    }

    holder.button.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Toast.makeText(getContext(), "Row " + position + " was clicked!", Toast.LENGTH_SHORT).show();
        }
    });

    return convertView;
}

This code will certainly work. But consider for a moment the downsides. Every single time that getView() is called, a brand new OnClickListener object is created and the old one thrown away. This is potentially adding a lot of garbage to the heap. There has to be a better way.

(Before we move on, let’s consider a couple of things that are going right in this example: re-using the convertView and using the ViewHolder pattern. ListView is highly optimized for buttery-smooth scrolling. One of the optimizations is that it re-uses views so that they don’t have to be re-created or re-inflated hundreds or thousands of times, eating up extra memory and wasting CPU cycles. But this optimization relies on you properly making use of the convertView in your own implementation of getView(). The ViewHolder pattern is a further optimization that lets you avoid having to call the relatively expensive findViewById() method over and over again. But… you already knew all this anyway, right?)

A Better Way — A single OnClickListener

An easy change is to take advantage of the fact that the onClick() callback in the OnClickListener gets passed a reference to the View that was clicked, giving us an opportunity to pass some information into the listener. So rather than hundreds of unique and transient click listeners, we can instead have one single click listener that knows how to find the data it needs. In order to accomplish this task, we’ll do something similar to the ViewHolder pattern and take advantage of the tag on the View that is being clicked:

@Override
public View getView(int position, View convertView, ViewGroup parent) {
    ViewHolder holder;
    if (convertView == null) {
        convertView = LayoutInflater.from(getContext()).inflate(R.layout.row_simple, parent, false);
        holder = new ViewHolder();
        holder.text = (TextView) convertView.findViewById(R.id.text);
        holder.button = (Button) convertView.findViewById(R.id.button);
        holder.button.setOnClickListener(mMyButtonClickListener);
        convertView.setTag(holder);
    }
    else {
        holder = (ViewHolder) convertView.getTag();
    }

    holder.button.setTag(position);

    return convertView;
}

private View.OnClickListener mMyButtonClickListener = new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        int position = (Integer) v.getTag();
        Toast.makeText(getContext(), "Row " + position + " was clicked!", Toast.LENGTH_SHORT).show();
    }
}

Notice how in this version, we have just a single click listener object (mMyButtonClickListener) that is connected to every instance of the button. We just tag a little bit of extra data on the button itself so that the click listener can figure out what to do.

An Alternate Better Way — A Click Listener in the ViewHolder

An alternate solution that Joe came up with and that I had not considered previously would be to put the click listener itself within the ViewHolder. This has the advantage that the click listener has easy access to the other View(s) within the row, making it very easy to change the state of those Views. For example, maybe you want to change the color of the text when the button is pressed:

@Override
public View getView(int position, View convertView, ViewGroup parent) {
    ViewHolder holder;
    if (convertView == null) {
        convertView = LayoutInflater.from(getContext()).inflate(R.layout.row_simple, parent, false);
        TextView tv = (TextView) convertView.findViewById(R.id.text);
        Button btn = (Button) convertView.findViewById(R.id.button);
        holder = new ViewHolder(tv, btn);
        convertView.setTag(holder);
    }
    else {
        holder = (ViewHolder) convertView.getTag();
    }

    holder.button.setTag(position);

    return convertView;
}

private static final int[] COLORS = new int[] { 0xffa00000, 0xff00a000, 0xff0000a0 };
private int mLastColor = 0;

private static class ViewHolder {
    public TextView text;
    public Button button;

    public ViewHolder(TextView tv, Button btn) {
        text = tv;
        button = btn;
        button.setOnClickListener(mMyLocalClickListener);
    }

    private View.OnClickListener mMyLocalClickListener = new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            text.setColor(COLORS[mLastColor]);
            mLastColor = (mLastColor + 1) % COLORS.length;
        }
    }
}

So, there you have it, three ways to do the same thing, each with its own plusses and minuses. As I mentioned earlier, the solution that works best for you will be highly dependent upon what you are trying to accomplish. But hopefully now you’ve got a few more tools in your toolbox to help you the next time you run into this type of issue.

Leave a Reply

Your email address will not be published. Required fields are marked *

     

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>