John Leehey's Blog

A brain dump for my experiences with code

An Alternative to the ViewHolder Pattern

In Android development, there’s no getting around learning how to correctly implement your own ListView adapter. A quick google search will teach you how to quickly spin up an ArrayAdapter, but usually devs tend to just roll their own extension of BaseAdapter.

In order to be efficient with the override of getView(), avoiding calls to findViewById() becomes necessary (findViewById is a relatively expensive call). This is where the ViewHolder pattern comes in. Google’s own docs expand on usage of the ViewHolder pattern, but for the last few years I’ve drifted towards taking a different approach.

Consider this simple ListView of users at a company:

You can imagine the model behind the list row views: String fields for the name and job title, and perhaps a Calendar object for the user’s date of birth.

User.java
1
2
3
4
5
6
public class User {
    public String name;
    public String jobTitle;
    public Calendar dateOfBirth;  
}

The resultant adapter along with the ViewHolder pattern will look something like this:

UserAdapter.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class UserAdapter extends BaseAdapter {

    static class ViewHolder {
        TextView name;
        TextView jobTitle;
        TextView dateOfBirth;
    }

    public User[] users;

    @Override
    public int getCount() {
        return users == null ? 0 : users.length;
    }

    @Override
    public User getItem(int position) {
        return users[position];
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {

        // Inflate the view
        if (convertView == null) {
            convertView = LayoutInflater.from(parent.getContext())
                    .inflate(R.layout.view_user_list_row, parent, false);
            ViewHolder holder = new ViewHolder();
            holder.name = (TextView) convertView.findViewById(R.id.user_name);
            holder.jobTitle = (TextView) convertView.findViewById(R.id.user_title);
            holder.dateOfBirth = (TextView) convertView.findViewById(R.id.user_age);
            convertView.setTag(holder);
        }

        ViewHolder holder = (ViewHolder) convertView.getTag();
        holder.name.setText(getItem(position).name);
        holder.jobTitle.setText(getItem(position).jobTitle);
        holder.dateOfBirth.setText(getItem(position).dateOfBirth.toString());

        return convertView;
    }
}

For a simple application, this may be OK. Having had experience with applications containing more than 15-20 listviews and activities though, this can get a little tedious. Reusing the view xml also means starting over with view referencing. Ideally, we’d like to encapsulate the view code into a single place, then attach the model in a single step. Here’s where we can take advantage of Java inheritance.

We’ll pull all our holder initialization code into a custom view. This view can subclass any class that extends View, but ideally we’ll want it to match the root xml node of the list row layout (I’ll explain why in a bit). Here’s an example user listrow subclass, extending from a RelativeLayout:

UserListRowView.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class UserListRowView extends RelativeLayout {

    private TextView tv_userName;
    private TextView tv_jobTitle;
    private TextView tv_userDateOfBirth;

    public UserListRowView(Context context) {
        super(context);
        init(context);
    }

    private void init(Context context) {
        LayoutInflater inflater = LayoutInflater.from(context);

        // Inflate the view into this object
        inflater.inflate(R.layout.view_user_list_row, this, true);

        tv_userName = (TextView) findViewById(R.id.user_name);
        tv_jobTitle = (TextView) findViewById(R.id.user_title);
        tv_userDateOfBirth = (TextView) findViewById(R.id.user_age);
    }

    /**
     * Set the data for the view, and populate the
     * children views with the model text.
     */
    public void setUser(User user) {
        tv_userName.setText(user.name);
        tv_jobTitle.setText(user.jobTitle);
        tv_userDateOfBirth.setText(
                DateFormat.getDateInstance(DateFormat.MEDIUM)
                        .format(user.dateOfBirth.getTime()));
    }
}

You can see at this point that the UserListRowView itself maintains the references to the children Views, and is for all intents and purposes acting as its own view holder. By subclassing the same View type as the root of our XML, we can substitute the root node with the <merge> tag. This will shorten the depth of the view hierarchy, which will save memory and allow those findViewById’s to work a little faster.

With our new UserListRowView object, we can tidy up getView() in the adapter.

UserAdapter.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class UserAdapter extends BaseAdapter {

    public User[] users;

    @Override
    public int getCount() {
        return users == null ? 0 : users.length;
    }

    @Override
    public User getItem(int position) {
        return users[position];
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {

        // Inflate the view
        if (convertView == null) {
            convertView = new UserListRowView(parent.getContext());
        }

        // We can now safely cast and set the data
        ((UserListRowView) convertView).setUser(getItem(position));

        return convertView;
    }
}

This also makes things easier because from now on, if there is a change to the model or the list row layout, the adapter and activity can remain untouched. In my opinion, it has always been a bit of a stretch to say Android development follows MVC, but this design pattern can certainly help bring it a step closer and save time down the road with those heavy projects.

It should be noted that there a few caveats to this design pattern:

  • Using the <merge> tag will ignore attributes set on the merge tag itself in the XML. This means that, when using , root node XML attributes will have to be interpreted and done programmatically in the custom view object. Another workaround would be to just not use <merge/>, and keep the root of the XML as a <RelativeLayout/> (keeping in mind that this will add a level to the view hierarchy).

  • If you need to reuse the custom view you’ve created in XML, you’ll need to override all the other default constructor types of the ViewGroup you are extending, and call init() for each. This isn’t as big of an issue though, especially if you get creative with some more inheritance.

If you’d like to see a full working sample with the code above, I’ve placed the example project on GitHub. Thanks for reading, enjoy!

Comments