博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

Android: Use the ViewHolder Luke, and Larry, and Curly, and Moe

Posted on 2011-04-17 11:15  thomas.lee  阅读(602)  评论(0编辑  收藏  举报

This is going to be a quick hit blog post about the Android "ViewHolder" pattern. This is a pattern that many people seem to be at least vaguely familiar with, but not very many actually use (based on open source Android applications and examples/books, etc.).

First some background. Larry, Moe, and Curly were characters from . . . ok, not that much background, but to really make use of the ViewHolder with Android you do need to know what the ListView widget is (a helper for managing views of lists), and that ListViews and generally backed by Adapters (adapters provide data for lists and build views for said data, they can be made from lists of stuff in files, or from in memory arrays, or from databases, and so on).

An example of a ListView in action is seen in the screen shot below.
DevNexus 2010 ListView

This screen is taken from the DevNexus 2010 demo application I wrote for a conference I attended and spoke at this year. This particular ListView is basically a navigable list of the presentation abstracts and speakers. We will look at the code behind this screen, complete with ViewHolder, coming up. First a bit more background.

You are probably at least vaguely familiar with ListView/Adapter and the related concepts if you have done any Android development at all. ListView is *very* powerful and helpful widget. Nevertheless, ListView's are often misused and abused because there are so many options and settings and patterns that surround them. I don't have time to go into great depth on ListView itself, but for the quick hit I promised I want to address 2 key things to keep in mind when working with ListViews:

  • Creating views is expensive, you don't want to inflate or manually create new views for every view on the list. ListView can re-use views, if you let it.
  • Even if you are re-using views, you also don't want to find child views by id (findViewById) every time, because that is expensive too.
  • I would say that 90% of Android examples or open source apps I see (or developers I talk to) do the first item there properly. Depending on the adapter in play they are re-using views. It's pretty simple, use the "convertView" on simple adapters like ArrayAdapter (which we will see coming up) and use the "newView" on more involved adapters like CursorAdapter. I would also say that the same high percentage of examples and open source apps *do not* use the ViewHolder to avoid trips to findViewById. If your list is small, this might not be a big deal, but if the list is large it can make a difference in terms of frame rate, resource usage, and performance (which equals better experience for the user, longer battery life, etc.). Also, it's a good habit to be in even with small lists, more efficient code is better, especially on a mobile platform.

    So how do you do this stuff? Well, here is the Adapter from the aforementioned DevNexus 2010 application to demonstrate:

    01 static class ViewHolder {
    02    private TextView view1;
    03    private TextView view2;
    04 }
    05  
    06 private class PresentationAdapter extends ArrayAdapter<Presentation&g<wbr>t; {
    07  
    08    LayoutInflater vi = (LayoutInflater) PresentationList.this.getSyste<wbr>mService(Context.LAYOUT_INFLAT<wbr>ER_SERVICE);
    09    private final ArrayList<Presentation> presentations;
    10  
    11    public PresentationAdapter(final Context context, final int resId, final ArrayList<Presentation> presentations) {
    12       super(context, resId, presentations);
    13       this.presentations = presentations;
    14    }
    15  
    16    @Override
    17    public View getView(final int position, final View convertView, final ViewGroup parent) {
    18       // re-use the convertView, try not to recreate objects here or inflate every time (expensive)
    19       // also use the "tag" with the "view holder" pattern to avoid findViewById every time
    20       View v = convertView;
    21       if (v == null) {
    22          v = vi.inflate(R.layout.list_items<wbr>_item, null);
    23          ViewHolder viewHolder = new ViewHolder();
    24          viewHolder.view1 = (TextView) v.findViewById(R.id.list_item_<wbr>above);
    25          viewHolder.view2 = (TextView) v.findViewById(R.id.list_item_<wbr>below);
    26          v.setTag(viewHolder);
    27       }
    28  
    29       Presentation p = presentations.get(position);
    30       if (p != null) {
    31          ViewHolder viewHolder = (ViewHolder) v.getTag();
    32          viewHolder.view1.setText(p.nam<wbr>e);
    33          viewHolder.view2.setText(p.spe<wbr>aker);           
    34       }
    35       return v;
    36    }
    37 }

    The complete code for the DevNexus application (for context), is here. What we are focusing on above is the Adapter to the ListView. Within it we see that we are making use of the convertView if present to grab our subsequent views, and to populate our own internal ViewHolder representation. If the convertView isn't present, then we do the extra work of inflating our views and building our ViewHolder.

    The "convertView" is a convenient helper object provided to you by the framework specifically for re-use (if the framework is ready to provide it, the first item in the list will have a null convertView, hence the check, and the work only if it is null). The framework knows the views that are passing by, and as it recycles them it hands you one that you can "convert."

    So what is the ViewHolder? Well, that's really just a simple object we have created (using a package scoped inner class) that stores child views (though you could store anything there, that is all we need for this example). View objects have a "Tag" property that you can use to store any object. If you are getting recycled views anyway, it's very easy and helpful to just stick child views you need *there* and retrieve later, as opposed to calling "findViewById" to pull views from resources for every view in the list. Really this just saves us the findViewById calls. That might not seem like much, but again, in large lists it can cut down on overhead.

    A little further down in the code, after we get past creating the convertView in the case that it's null, we see where we use the ViewHolder to get a reference to our subsequent child views, and set stuff (in this case just Strings, but could be anything).

    This is a simple example, and I certainly won't promise that it's perfect, but hopefully it does help to explain what ViewHolder is, and why it's useful (and a little about re-cycling views in general).

    Comments

    Alternative to the ViewHolder

    Thanks for writing on this subject. In the past, I did not bother with caching the find results and after reading this blog, I decided to give it a try and see if it improves the speed of my app any. I noticed my app become more responsive and that's all the convincing I need to keep it.

    I do, however, have a different approach to caching than how you wrote this article. Instead of keeping a separate class in which to store the result of the various finds, I just store it directly into the View.tag mechanism like so:

    1 protected Object getViewHandle(View aParentView, int aViewToFind) {
    2     Object v = aParentView.getTag(aViewToFind<wbr>);
    3     if (v==null) {
    4         v = aParentView.findViewById(aView<wbr>ToFind);
    5         aParentView.setTag(aViewToFind<wbr>,v);        
    6     }
    7     return v;
    8 }

    and then call it in my code by

    1 TextView tv = (TextView)getViewHandle(itemVi<wbr>ew,R.id.some_text_view);

    This way I don't need to define classes for each kind of item view in the list and makes the technique easily adaptable for different lists without introducing another class that needs to be passed around to all the code that works with such views.

    Thanks for the inspiration!