ViewHolder considered harmful

转载自:http://blog.xebia.com/2013/07/22/viewholder-considered-harmful/


The ListView is the most complicated, common view widget in the Android SDK. It is part of a family of widgets known as Adapter Views. These are views that use an Adapter class to mediate between the view widget and the list data it is showing.

ViewHolder considered harmful_第1张图片

The adapter’s job is to prepare the views to be shown in the ListView for each element in the dataset. This tends to involve many findViewById(int) lookups, which are quite costly in CPU time. The standing Best Practices for Android dictate that adapters use something called the ViewHolder Pattern to mitigate the cost of these lookups.

Adapter classes tend to be a hotspot of smelly program code in Android, because they straddle the View and Controller responsibilities of the MVC model (1). In my part of the Clean Code in Android Apps talk at XebiCon 2013 I demonstrated why adapters get smelly, why ViewHolder doesn’t help and how to use a custom view to alleviate this problem. Read on for the reprise.

The problem

Consider the following application:

The screenshot shows a simple contact list application. The main screen is a single ListView and the list items follow just three formatting rules:

  • The email and street address fields are hidden if they have no data.
  • If there’s no name in the Contact record, the email address is shown in the name field and the email address field is hidden.
  • If there’s neither a name nor an email address in the Contact record, a default value is shown in the name field.

These simple rules result in a getView method on the ContactListAdapter that’s already about thirty lines of code:

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
49
50
51
52
// https://github.com/xebia/xebicon-2013__cc-in-aa/blob/1-_naive_adapter/src/com/xebia/xebicon2013/cciaa/ContactListAdapter.java
 
     public View getView( int position, View convertView, ViewGroup parent) {
         final Contact item = getItem(position);
         final View view = (convertView == null )
             ? inflater.inflate(R.layout.list_item, null )
             : convertView;
 
         TextView nameView = ((TextView) view.findViewById(R.id.contact_name));
         if (item.getName() != null ) {
             nameView.setText(item.getName());
         } else if (item.getEmail() != null ) {
             nameView.setText(item.getEmail());
         } else {
             nameView.setText(R.string.unidentified);
         }
 
         TextView emailView = (TextView) view.findViewById(R.id.contact_email);
         if (item.getEmail() != null ) {
             emailView.setText(item.getEmail());
             emailView.setVisibility(item.getName() == null ? View.GONE : View.VISIBLE);
         } else {
             emailView.setVisibility(View.GONE);
         }
 
         TextView addressView = (TextView) view.findViewById(R.id.contact_address);
         if (item.getAddressLines() != null ) {
             addressView.setText(item.getAddressLines());
             addressView.setVisibility(View.VISIBLE);
         } else {
             addressView.setVisibility(View.GONE);
         }
         return view;
     }

Now imagine more fields in your child view, more child types in your adapter and more sets of formatting rules. The amount of formatting code grows exponentially. You can use the extract method refactoring to cut the getView method into smaller parts, but that’s fighting the symptoms while ignoring the cause. The real problem is that the code is badly structured, and this is apparent when you visualize which components interact:

The ViewHolder pattern is no solution

The preceding example does not use the ViewHolder Pattern mentioned in the introduction. The best practices of using adapter views tell you to use this pattern and it’s pretty popular. A ViewHolder is a helper class that holds references to all children of your list item view. It is itself kept in the tag of the root view of each list item. This way, you have to populate these references only once. Here’s a typical ViewHolder class (click to expand):

?
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// https://github.com/xebia/xebicon-2013__cc-in-aa/blob/2-_ViewHolder_pattern/src/com/xebia/xebicon2013/cciaa/ViewHolder.java
 
public class ViewHolder {
     public final TextView nameView;
     public final TextView emailView;
     public final TextView addressView;
 
     public ViewHolder(View listItem) {
         nameView = (TextView) listItem.findViewById(R.id.contact_name);
         emailView = (TextView) listItem.findViewById(R.id.contact_email);
         addressView = (TextView) listItem.findViewById(R.id.contact_address);
         listItem.setTag( this );
     }
}

Here’s what ContactListAdapter looks like when implemented using the ViewHolder Pattern (click to expand):

?
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
49
50
51
52
53
54
55
// https://github.com/xebia/xebicon-2013__cc-in-aa/blob/2-_ViewHolder_pattern/src/com/xebia/xebicon2013/cciaa/ContactListAdapter.java
 
     public View getView( int position, View convertView, ViewGroup parent) {
         ViewHolder holder;
         if (convertView == null ) {
             convertView = inflater.inflate(R.layout.list_item, null );
             holder = new ViewHolder(convertView);
         } else {
             holder = (ViewHolder) convertView.getTag();
         }
         Contact item = getItem(position);
         String name = item.getName();
         String email = item.getEmail();
         String address = item.getAddressLines();
         if (name != null ) {
             holder.nameView.setText(name);
         } else if (email != null ) {
             holder.nameView.setText(email);
         } else {
             holder.nameView.setText(R.string.unidentified);
         }
         if (email != null ) {
             holder.emailView.setText(email);
             holder.emailView.setVisibility(name == null ? View.GONE : View.VISIBLE);
         } else {
             holder.emailView.setVisibility(View.GONE);
         }
         if (address != null ) {
             holder.addressView.setText(address);
             holder.addressView.setVisibility(View.VISIBLE);
         } else {
             holder.addressView.setVisibility(View.GONE);
         }
         return convertView;
     }

ViewHolder doesn’t simplify the code in the adapter class. It is a performance optimization, to avoid the cost of repeated findViewById(int) lookups. A glance at the component structure reveals that it makes things more complicated:

A custom ViewGroup lets you eat your cake and have it

You can do much better by using a Custom View Group for your list items. You get the benefits of ViewHolder and the clarity of your code is improved. This is all that remains of the original getView method:

21
22
23
24
25
26
27
28
29
30
31
32
33
// https://github.com/xebia/xebicon-2013__cc-in-aa/blob/3-_custom_ViewGroup/src/com/xebia/xebicon2013/cciaa/ContactListAdapter.java
 
public View getView( int position, View convertView, ViewGroup parent) {
         ContactView view;
         if (convertView == null ) {
             view = (ContactView) inflater.inflate(R.layout.list_item, null );
         } else {
             view = (ContactView) convertView;
         }
         Contact item = getItem(position);
         view.showContact(item);
         return view;
     }

This gain is made by separating the View and Controller responsibilities. The new View class has a public API defined entirely in terms from your domain model, freeing the adapter class to handle only the Controller responsibility. The structure diagram reveals this simplicity:

How do you create a custom view group?

Creating a custom view group is just like creating a custom view. You start by creating a subclass of an existing View class. In our example the root element of list_item.xml is <LinearLayout, so we extend the android.widget.LinearLayout class. You only need to add the superclass constructors to get a functioning custom view:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ContactView extends LinearLayout {
     /** Inherited constructor. */
     public ContactView(Context context) {
         super (context);
     }
 
     /** Inherited constructor. */
     public ContactView(Context context, AttributeSet attrs) {
         super (context, attrs);
     }
 
     /** Inherited constructor. */
     public ContactView(Context context, AttributeSet attrs, int defStyle) {
         super (context, attrs, defStyle);
     }
}

To integrate it into the layout, simply change the root tag of the layout XML from <LinearLayout to the full class name of the custom view group, <com.xebia.xebicon2013.cciaa.ContactView. Here’s layout/list_item.xml as modified to use the new custom view group:

1
2
3
4
5
6
7
8
9
<!-- https://github.com/xebia/xebicon-2013__cc-in-aa/blob/3-_custom_ViewGroup/res/layout/list_item.xml --> </ p >
< com.xebia.xebicon2013.cciaa.ContactView
         xmlns:android = "http://schemas.android.com/apk/res/android"
         android:layout_width = "match_parent"
         android:orientation = "vertical"
         android:layout_height = "match_parent" >
 
     <!-- view group contents unchanged -->
</ com.xebia.xebicon2013.cciaa.ContactView >

A custom view with only the constructors is pointless. To take advantage of the custom view class, implement the onFinishInflate() callback to look up the view references and put your application code in the rest of the class. In this case, that’s the showContact(Contact) method we’ve seen in our earlier adapters (click to expand):

?
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
// https://github.com/xebia/xebicon-2013__cc-in-aa/blob/3-_custom_ViewGroup/src/com/xebia/xebicon2013/cciaa/ContactView.java
 
public class ContactView extends LinearLayout {
 
     public static final Contact EMPTY = new Contact( null , null , null , null );
     private TextView nameView;
     private TextView emailView;
     private TextView addressView;
     private Contact contact = EMPTY;
 
     /** Inherited constructors as before. */
 
     @Override
     protected void onFinishInflate() {
         super .onFinishInflate();
         nameView = (TextView) findViewById(R.id.contact_name);
         emailView = (TextView) findViewById(R.id.contact_email);
         addressView = (TextView) findViewById(R.id.contact_address);
     }
 
     public void showContact(Contact contact) {
         this .contact = (contact != null ? contact : EMPTY);
         String name = contact.getName();
         String email = contact.getEmail();
         String address = contact.getAddressLines();
         if (name != null ) {
             nameView.setText(name);
         } else if (email != null ) {
             nameView.setText(email);
         } else {
             nameView.setText(R.string.unidentified);
         }
         if (email != null ) {
             emailView.setText(email);
             emailView.setVisibility(name == null ? View.GONE : View.VISIBLE);
         } else {
             emailView.setVisibility(View.GONE);
         }
         if (address != null ) {
             addressView.setText(address);
             addressView.setVisibility(View.VISIBLE);
         } else {
             addressView.setVisibility(View.GONE);
         }
     }
}

To summarise

The custom view group approach has a number of structure advantages over the ViewHolder Pattern:

  • It exhibits higher cohesion and lower coupling than the ViewHolder approach.
  • The adapter class operates at its natural level of abstraction —entire child views—, without diving into low-level details.
  • The code no longer relies on the untyped and externally modifiable tag property of the root View.
  • The detailed conditional logic isn’t gone (it’s essential complexity), but it’s been restricted to the narrowest possible scope. Some of this complexity could be pushed down into further custom views.
  • If the same view group is used as a child in more than one adapter, none of the low-level code needs to be copied into the new adapter.

A custom view group has only advantages to ViewHolder, because the performance benefit of avoiding unneccessary findViewById(int) lookups is preserved. The number of custom classes is the same and there’s one less object instance.

Does all this mean that ViewHolder Pattern is actually Harmful? That’s too harsh. Nevertheless, the benefits of using a custom ViewGroup are such that I believe it’s time to retire the ViewHolder Pattern.

The full sample code for this article is on github: xebia/xebicon-2013__cc-in-aa. The three approaches are on three different branches: 1-_naieve_adapter, 2-_ViewHolder_Pattern and 3-_custom_ViewGroup.

Updated, July 30: A further refinement of this technique is published in the follow-up post A better custom ViewGroup.

1) I use “MVC” as the name of the pattern family, which includes MVP and MVVM as well as MVC. Which one Android implements is an exercise for the reader.




Jerome P. commented on my earlier post denouncing the ViewHolder pattern. He suggested an improvement to the ViewGroup approach that inverts the direction of dependency between the ContactView and its layout XML file. Instead of referencing the custom class from XML, the Java code now references the layout file using a standard resource reference. This adds further type safety and it means your view class can be completely ProGuarded.

The list adapter implementation becomes more convenient. It no longer needs to use a layout inflater, it can simply new up an instance of the view class:

1
2
3
4
5
6
7
8
public View getView( int position, View convertView, ViewGroup parent) {
     ContactView view;
     if (convertView == null ) {
         view = new ContactView(context);
         // Was: view = (ContactView) inflater.inflate(R.layout.list_item, null);
     }
     // continued...
}

To do this, change the root element of the XML layout to <merge/> and modify the constructors of the custom view group:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ContactView extends LinearLayout {
 
     // private field declarations
 
     /** Inherited constructor. */
     public ContactView(Context context) {
         super (context);
         init(context);
     }
 
     /** All three constructors invoke this method. */
     private void init(Context context) {
         setOrientation(VERTICAL);
         LayoutInflater.from(context).inflate(R.layout.contact_view, this , true );
         nameView = (TextView) findViewById(R.id.contact_name);
         emailView = (TextView) findViewById(R.id.contact_email);
         addressView = (TextView) findViewById(R.id.contact_address);
     }
 
     // continued

The complete code for this example has been added to the 4-_better_custom_ViewGroup branch of the android-cciaa repository on GitHub.


你可能感兴趣的:(android,ViewGroup,viewholder)