December 20, 2014

Return of the Fancy ListViews

BuildingDroidsLong-time followers of this column may recall a blog post series I did back in the summer of 2008, covering “fancy ListViews“. This referred to being able to take control over what rows look like in a ListView, so you can tailor each to appear exactly as you want. Today, let’s revisit the topic, in light of some changes in recent Android SDK releases.

You may recall that one of the techniques those earlier posts covered was the use of a “holder” or “wrapper”. This cached the results of findViewById() lookups in a custom class, an instance of which was stored in each row View’s “tag” via setTag(). When a row is recycled, you can grab the wrapper via getTag(), then use the wrapper to access the individual widgets inside of the row (e.g., ImageView, TextView). On a list that may be extensively scrolled, this saves hundreds, if not thousands, of findViewById() calls, which are not exactly cheap.

Android 1.6 quietly added a pair of new methods to the View class which can simplify the creation of fancy lists like the ones shown in this chapter. Specifically, there are two new versions of getTag() and setTag() that take an identifier along with their object. These let you eliminate the wrapper class while still reaping much of its benefits.

Below, you will see some code from a sample project that uses these new getTag()/setTag() methods to cache findViewById() lookups:

[java]
package com.commonsware.android.fancylists.eight;

import android.app.Activity;
import android.os.Bundle;
import android.app.ListActivity;
import android.view.View;
import android.view.ViewGroup;
import android.view.LayoutInflater;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;

public class SelfWrapperDemo extends ListActivity {
TextView selection;
String[] items={"lorem", "ipsum", "dolor", "sit", "amet",
"consectetuer", "adipiscing", "elit", "morbi", "vel",
"ligula", "vitae", "arcu", "aliquet", "mollis",
"etiam", "vel", "erat", "placerat", "ante",
"porttitor", "sodales", "pellentesque", "augue",
"purus"};

@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
setContentView(R.layout.main);
setListAdapter(new IconicAdapter());
selection=(TextView)findViewById(R.id.selection);
}

private String getModel(int position) {
return(((IconicAdapter)getListAdapter()).getItem(position));
}

public void onListItemClick(ListView parent, View v,
int position, long id) {
selection.setText(getModel(position));
}

class IconicAdapter extends ArrayAdapter<String> {
IconicAdapter() {
super(SelfWrapperDemo.this, R.layout.row, items);
}

public View getView(int position, View convertView,
ViewGroup parent) {
View row=convertView;
if (row==null) {
LayoutInflater inflater=getLayoutInflater();
row=inflater.inflate(R.layout.row, parent, false);
row.setTag(R.id.label, row.findViewById(R.id.label));
row.setTag(R.id.icon, row.findViewById(R.id.icon));
}

TextView label=(TextView)row.getTag(R.id.label);
ImageView icon=(ImageView)row.getTag(R.id.icon);
label.setText(getModel(position));

if (getModel(position).length()>4) {
icon.setImageResource(R.drawable.delete);
}
else {
icon.setImageResource(R.drawable.ok);
}
return(row);
}
}
}
[/java]

When we inflate our row, we also pull out all widgets we might need to modify at runtime (R.id.label and R.id.icon) and stash those widgets as “tags” under their respective IDs. Then, regardless of row source, we pull out the widgets from those tags and update them. We gain the widget-level caching that a wrapper would provide us, but we do not need an actual wrapper class.

There are three caveats:

  • This technique does not offer any of the other potential benefits of a wrapper class, such as lazy-fetching
  • The index provided to getTag() and setTag() must be an identifier — you cannot just use 0, 1, etc. like you might in an ArrayList
  • This only works on Android 1.6…though there are tricks to get this same sort of logic working on Android 1.5 via reflection, a topic for another time

NOTE: this blog post is derived from a section in The Busy Coder’s Guide to Android Development, Version 2.8. A free excerpt from this book, covering all of the “fancy ListView” topics, can be downloaded as a PDF from the CommonsWare site.



  • http://broschb.blogspot.com Brandon

    when you do/display tutorials there should be an image of the final product. It's nice to see what I'm getting when following a tutorial.

    • http://intensedebate.com/people/commonsguy commonsguy

      This isn't strictly a tutorial, more of a code sample illustrating a point.

  • Romain Guy

    The new "tags" API is not meant to be used this way and queried often. It may (potentially) be expensive to use. You should stick to the view holder pattern (and, if I may be annoying, please refer to it as the view holder pattern, and not "wrapper," since it's not a wrapper at all and that's not how our examples call it.)

    • http://intensedebate.com/people/commonsguy commonsguy

      Uh, OK. What the heck is it there for, if not this? Considering the key has to be an ID, and the limited set of use cases for View tags in the first place, this seemed like the logical explanation.

      • Dianne

        This was added to allow different entities to associate their own tags with the view, without trampling on each other. The specific case this is addressing is some places in the framework where we want to associate a tag with a view, but that would conflict with an application if it also wanted to set a tag. So applications should continue to use the single fast tag; the framework (and other libraries that will be used by applications) should use tags with identifiers.

        • http://intensedebate.com/people/commonsguy commonsguy

          Ah. That makes sense, though tying keys to R. values seems to be an odd restriction in this case. Many "other libraries" won't have R. values of their own, because, well, they're libraries and therefore don't have resources. I can see how you were aiming for it to be a unique value that way, and I'm not sure how else you could do it, but it does mean I can't use the new getTag()/setTag() in a reusable JAR unless I require a unique ID to be given to my by the application via a method on some object, which may or may not be convenient for me or the application.

          All that being said, I understand now the role the new getTag()/setTag() are supposed to play. Thanks for the info!

  • Bill

    "…can be downloaded as a PDF from the CommonsWare site."

    Follow the link and I get: 403 Forbidden

    –Bill

    • http://intensedebate.com/people/commonsguy commonsguy

      Sorry about that — it's been fixed.

  • kwo

    The <code> tags in this post are being displayed as code snippets, not inline, and are therefore making this post all but unreadable.

  • Marapes

    Yes, I am afraid these posts are not friendly user at all, their style is horrible :-/

  • Marapes

    I mean their CSS styling, not the post's style. The same text with a good css layout and some pics would be simply great.