Blog

ViewHolder als schädlich angesehen

Barend Garvelink

Aktualisiert Oktober 22, 2025
8 Minuten

Die ListView ist das komplizierteste, gängigste View-Widget im Android SDK. Es gehört zu einer Familie von Widgets, die als Adapter Views bekannt sind. Dies sind Ansichten, die einen Adapter Klasse verwenden, um zwischen dem View-Widget und den angezeigten Listendaten zu vermitteln.

ListView, ListAdapter und Liste vom Typ DataType.

Die Aufgabe des Adapters besteht darin, die in der ListView anzuzeigenden Ansichten für jedes Element des Datensatzes vorzubereiten. Dazu sind in der Regel viele findViewById(int) -Aufrufe erforderlich, die ziemlich viel CPU-Zeit kosten. Die ständigen Best Practices für Android schreiben vor, dass Adapter das so genannte ViewHolder-Muster verwenden, um die Kosten für diese Suchvorgänge zu verringern.

Adapterklassen sind in der Regel ein Hotspot für übel riechenden Programmcode in Android, da sie die Aufgaben von View und Controller des MVC-Modells überspannen (1). In meinem Teil des Vortrags über sauberen Code in Android-Apps auf der XebiCon 2013 habe ich gezeigt, warum Adapter stinken, warum ViewHolder nicht helfen und wie man eine benutzerdefinierte Ansicht verwendet, um dieses Problem zu lindern. Lesen Sie weiter für die Wiederholung.

Das Problem

Betrachten Sie die folgende Anwendung:

Screenshot einer Android-App, die eine Adressliste mit fünf Kontakten zeigt, die alle unterschiedlich dargestellt werden, je nachdem, welche Daten bekannt sind.

Der Screenshot zeigt eine einfache Anwendung für eine Kontaktliste. Der Hauptbildschirm ist eine einzelne ListView und die Listenelemente folgen nur drei Formatierungsregeln:

  • Die Felder E-Mail und Adresse werden ausgeblendet, wenn sie keine Daten enthalten.
  • Wenn im Kontaktdatensatz kein Name vorhanden ist, wird die E-Mail-Adresse im Namensfeld angezeigt und das E-Mail-Adressfeld ist ausgeblendet.
  • Wenn im Kontaktdatensatz weder ein Name noch eine E-Mail-Adresse vorhanden ist, wird im Feld Name ein Standardwert angezeigt.

Diese einfachen Regeln führen zu einer getView-Methode für den ContactListAdapter, die bereits etwa dreißig Zeilen Code umfasst:

// 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;
}

Stellen Sie sich nun mehr Felder in Ihrer untergeordneten Ansicht, mehr untergeordnete Typen in Ihrem Adapter und mehr Sätze von Formatierungsregeln vor. Die Menge des Formatierungscodes wächst exponentiell. Sie können die Methode extract refactoring verwenden, um die Methode getView in kleinere Teile zu zerlegen, aber damit bekämpfen Sie nur die Symptome und ignorieren die Ursache. Das eigentliche Problem ist, dass der Code schlecht strukturiert ist, und das wird deutlich, wenn Sie sich ansehen, welche Komponenten zusammenwirken:

Abhängigkeitsstrukturdiagramm, das zeigt, dass ListAdapter eine gemischte Verantwortung hat und direkt von Kompositionselementen der View-Klasse abhängig ist.

Das ViewHolder-Muster ist keine Lösung

Das vorangegangene Beispiel verwendet nicht das in der Einführung erwähnte ViewHolder-Muster. Die Best Practices für die Verwendung von Adapteransichten raten Ihnen, dieses Muster zu verwenden, und es ist ziemlich beliebt. Ein ViewHolder ist eine Hilfsklasse, die Verweise auf alle untergeordneten Elemente Ihrer Listenansicht enthält. Sie wird selbst in dem Tag der Stammansicht eines jeden Listenelements gespeichert. Auf diese Weise müssen Sie diese Verweise nur einmal ausfüllen. Hier ist eine typische ViewHolder-Klasse (zum Vergrößern klicken):

[code collapse="true" language="java" firstline="4"]
// 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 );
}
}
[/code]

So sieht der ContactListAdapter aus, wenn er mit dem ViewHolder-Muster implementiert wurde (zum Erweitern klicken):

[code collapse="true" language="java" firstline="21"]
// 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;
}
[/code]

ViewHolder vereinfacht den Code in der Adapterklasse nicht. Es ist eine Leistungsoptimierung, um die Kosten für wiederholte findViewById(int) Lookups zu vermeiden. Ein Blick auf die Komponentenstruktur zeigt, dass sie die Dinge komplizierter macht:

Das Diagramm der Abhängigkeitsstruktur zeigt, dass das Hinzufügen eines ViewHolders den Objektgraphen nur verkompliziert.

Mit einer benutzerdefinierten ViewGroup können Sie Ihren Kuchen essen und ihn haben

Sie können viel mehr erreichen, wenn Sie eine benutzerdefinierte Ansichtsgruppe für Ihre Listenelemente verwenden. Sie erhalten die Vorteile von ViewHolder und die Übersichtlichkeit Ihres Codes wird verbessert. Dies ist alles, was von der ursprünglichen getView-Methode übrig geblieben ist:

[code language="java" firstline="21"]
// 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;
}
[/code]

Dieser Gewinn wird durch die Trennung der Verantwortlichkeiten von View und Controller erzielt. Die neue View-Klasse verfügt über eine öffentliche API, die vollständig in den Begriffen Ihres Domänenmodells definiert ist, so dass die Adapterklasse nur noch für den Controller zuständig ist. Das Struktogramm verdeutlicht diese Einfachheit:

Das Diagramm der Abhängigkeitsstruktur zeigt, dass eine benutzerdefinierte ViewGroup für eine saubere MVC-Trennung sorgt.

Wie erstellen Sie eine benutzerdefinierte Ansichtsgruppe?

Das Erstellen einer benutzerdefinierten Ansichtsgruppe ist genau wie das Erstellen einer benutzerdefinierten Ansicht. Sie beginnen mit der Erstellung einer Unterklasse einer bestehenden View-Klasse. In unserem Beispiel ist das Root-Element von list_item.xml <LinearLayout, also erweitern wir die android.widget.LinearLayout Klasse. Sie müssen nur die Konstruktoren der Superklasse hinzufügen, um eine funktionierende benutzerdefinierte Ansicht zu erhalten:

[code language="java"]
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);
}
}
[/code]

Um sie in das Layout zu integrieren, ändern Sie einfach das Root-Tag des Layout-XML von <LinearLayout in den vollständigen Klassennamen der benutzerdefinierten Ansichtsgruppe, <com.xebia.xebicon2013.cciaa.ContactView. Hier ist layout/list_item.xml in der geänderten Form, um die neue benutzerdefinierte Ansichtsgruppe zu verwenden:

[code language="xml" highlight="2"]




[/code]

Eine benutzerdefinierte Ansicht mit nur den Konstruktoren ist sinnlos. Um die Vorteile der benutzerdefinierten View-Klasse zu nutzen, implementieren Sie den onFinishInflate() -Callback, um die View-Referenzen nachzuschlagen und Ihren Anwendungscode im Rest der Klasse unterzubringen. In diesem Fall ist das die Methode showContact(Contact), die wir in unseren früheren Adaptern gesehen haben (zum Erweitern klicken):

[code collapse="true" language="java"]
// 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);
}
}
}
[/code]

Zusammengefasst

Der Ansatz der benutzerdefinierten Ansichtsgruppe hat eine Reihe von Strukturvorteilen gegenüber dem ViewHolder-Muster:

  • Er weist eine höhere Kohäsion und geringere Kopplung auf als der ViewHolder-Ansatz.
  • Die Adapterklasse arbeitet auf ihrer natürlichen Abstraktionsebene -ganze untergeordnete Ansichten-, ohne in Details auf niedriger Ebene einzutauchen.
  • Der Code verlässt sich nicht mehr auf die nicht typisierte und extern änderbare Tag-Eigenschaft der Stammansicht.
  • Die detaillierte bedingte Logik ist nicht verschwunden (sie ist eine wesentliche Komplexität), aber sie wurde auf den kleinstmöglichen Umfang beschränkt. Ein Teil dieser Komplexität könnte in weitere benutzerdefinierte Ansichten verlagert werden.
  • Wenn dieselbe Ansichtsgruppe als untergeordnetes Element in mehr als einem Adapter verwendet wird, muss kein Low-Level-Code in den neuen Adapter kopiert werden.

Eine benutzerdefinierte Ansichtsgruppe hat nur Vorteile gegenüber ViewHolder, da der Leistungsvorteil durch die Vermeidung unnötiger findViewById(int) Lookups erhalten bleibt. Die Anzahl der benutzerdefinierten Klassen ist die gleiche und es gibt eine Objektinstanz weniger.

Bedeutet all dies, dass das ViewHolder-Muster tatsächlich schädlich ist? Das wäre zu hart. Dennoch sind die Vorteile der Verwendung einer benutzerdefinierten ViewGroup so groß, dass es meiner Meinung nach an der Zeit ist, das ViewHolder-Muster in Rente zu schicken.

Den vollständigen Beispielcode für diesen Artikel finden Sie auf github: xebia/xebicon-2013cc-in-aa.

Die drei Ansätze beziehen sich auf drei verschiedene Zweige:

Github - xebia/xebicon-2013cc-in-aa/tree/1-_naive_adapter 1-_naieve_adapter,

2-_ViewHolder_Pattern und 3-_custom_ViewGroup.

Aktualisiert, 30. Juli: Eine weitere Verfeinerung dieser Technik finden Sie in dem Folgebeitrag Eine bessere benutzerdefinierte ViewGroup.

1) Ich verwende "MVC" als den Namen der Musterfamilie, die neben MVC auch MVP und MVVM umfasst. Welche davon Android implementiert, ist eine Übung für den Leser.

Verfasst von

Barend Garvelink

Contact

Let’s discuss how we can support your journey.