web-dev-qa-db-fra.com

FindViewById vs View Holder Pattern dans l'adaptateur ListView

J'utilise toujours LayoutInflater et findViewById pour créer un nouvel élément dans la méthode getView d'un Adapter.

Mais dans de nombreux articles, les gens écrivent que findViewById est très très lent et recommande fortement d'utiliser le modèle de support de vue.

Quelqu'un peut-il expliquer pourquoi findViewById est si lent? Et pourquoi le View Holder Pattern est plus rapide?

Et que dois-je faire si nécessaire pour ajouter différents éléments à un ListView? Dois-je créer des classes pour chaque type?

static class ViewHolderItem1 {
    TextView textViewItem;
}

static class ViewHolderItem2 {
    Button btnViewItem;
}
static class ViewHolderItem3 {
    Button btnViewItem;
    ImageView imgViewItem;
}
49
Suvitruf

Quelqu'un peut-il expliquer pourquoi findViewById est si lent? Et pourquoi View Holder Pattern est plus rapide?

Lorsque vous n'utilisez pas Holder, la méthode getView() appellera findViewById() autant de fois que vos lignes seront hors de vue. Donc, si vous avez 1 000 lignes dans List et 990 lignes seront hors de vue, 990 fois seront appelées findViewById().

Le modèle de conception de Holder est utilisé pour la mise en cache de View - L'objet Holder (arbitraire) contient les widgets enfants de chaque ligne et lorsque la ligne est hors de View, findViewById () ne sera pas appelé mais View sera recyclé et les widgets seront obtenus auprès de Holder.

if (convertView == null) {
   convertView = inflater.inflate(layout, null, false);
   holder = new Holder(convertView);
   convertView.setTag(holder); // setting Holder as arbitrary object for row
}
else { // view recycling
   // row already contains Holder object
   holder = (Holder) convertView.getTag();
}

// set up row data from holder
titleText.setText(holder.getTitle().getText().toString());

Où la classe Holder peut ressembler:

public class Holder {

   private View row;
   private TextView title;

   public Holder(View row) {
      this.row = row;
   }

   public TextView getTitle() {
      if (title == null) {
         title = (TextView) row.findViewById(R.id.title);
      }
      return title;
   }
}

Comme @meredrica l'a indiqué si vous voulez obtenir de meilleures performances, vous pouvez utiliser des champs publics (mais cela détruit l'encapsulation).

Mise à jour:

Voici la deuxième approche pour utiliser le modèle ViewHolder:

ViewHolder holder;
// view is creating
if (convertView == null) {
   convertView = LayoutInflater.from(mContext).inflate(R.layout.row, parent, false);
   holder = new ViewHolder();   
   holder.title = (TextView) convertView.findViewById(R.id.title);
   holder.icon = (ImageView) convertView.findViewById(R.id.icon);
   convertView.setTag(holder);
}
// view is recycling
else {
   holder = (ViewHolder) convertView.getTag();
}

// set-up row
final MyItem item = mItems.get(position);
holder.title.setText(item.getTitle());
...

private static class ViewHolder {

   public TextView title;
   public ImageView icon;
}

Mise à jour # 2:

Comme tout le monde le sait, Google et AppCompat v7 en tant que bibliothèque de support ont publié un nouveau ViewGroup appelé RecyclerView qui est conçu pour le rendu de toutes les vues basées sur l'adaptateur. Comme @ antonioleiva dit dans post : "Il est supposé être le successeur de ListView et GridView".

Pour pouvoir utiliser cet élément, vous avez essentiellement besoin d'une des choses les plus importantes et c'est un type spécial d'adaptateur qui est enveloppé dans ViewGroup mentionné - RecyclerView.Adapter où ViewHolder est la chose dont nous parlons ici :) Simplement, ce nouvel élément ViewGroup a son propre modèle ViewHolder implémenté. Tout ce que vous devez faire est de créer une classe ViewHolder personnalisée qui doit s'étendre à partir de RecyclerView.ViewHolder et vous n'avez pas besoin de vous soucier de vérifier si le courant la ligne de l'adaptateur est nulle ou non.

L'adaptateur le fera pour vous et vous pouvez être sûr que la rangée ne sera gonflée que dans le cas où elle doit être gonflée (je dirais). Voici une implémentation simple:

public static class ViewHolder extends RecyclerView.ViewHolder {

   private TextView title;

   public ViewHolder(View root) {
      super(root);
      title = root.findViewById(R.id.title);
   }
}

Deux choses importantes ici:

  • Vous devez appeler le constructeur super () dans lequel vous devez passer votre vue racine de la ligne
  • Vous pouvez obtenir la position spécifique de la ligne directement à partir de ViewHolder via la méthode getPosition () . Ceci est utile lorsque vous souhaitez effectuer une action après avoir appuyé sur1 widget sur la ligne.

Et une utilisation de ViewHolder dans l'adaptateur. L'adaptateur a trois méthodes que vous devez implémenter:

  • onCreateViewHolder () - où ViewHolder est créé
  • onBindViewHolder () - où vous mettez à jour votre ligne. Nous pouvons dire que c'est un morceau de code où vous recyclez la ligne
  • getItemCount () - je dirais que c'est la même chose que la méthode getCount () typique dans BaseAdapter

Donc un petit exemple:

@Override 
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
   View root = LayoutInflater.from(mContext).inflate(myLayout, parent, false);
   return new ViewHolder(root);
}

@Override public void onBindViewHolder(ViewHolder holder, int position) {
   Item item = mItems.get(position);
   holder.title.setText(item.getTitle());
}

@Override public int getItemCount() {
   return mItems != null ? mItems.size() : 0;
}

1 Il est bon de mentionner que RecyclerView ne fournit pas d'interface directe pour pouvoir écouter les événements de clic d'élément. Cela peut être curieux pour quelqu'un mais voici une belle explication pourquoi ce n'est pas aussi curieux qu'il n'y paraît.

J'ai résolu cela en créant ma propre interface qui est utilisée pour gérer les événements de clic sur les lignes (et tout type de widget que vous souhaitez en ligne):

public interface RecyclerViewCallback<T> {

   public void onItemClick(T item, int position); 
}

Je le lie à Adapter via le constructeur, puis appelle ce rappel dans ViewHolder:

root.setOnClickListener(new View.OnClickListener {
   @Override
   public void onClick(View v) {
      int position = getPosition();
      mCallback.onItemClick(mItems.get(position), position);
   }
});

Ceci est un exemple de base, ne le prenez donc pas comme une seule façon possible. Les possibilités sont infinies.

85
Simon Dorociak

ViewHolder modèle créera une instance statique de ViewHolder et l'attachera à l'élément de vue la première fois qu'il sera chargé, puis il sera récupéré de cette balise de vue lors des prochains appels. comme nous le savions la méthode getView () est appelée très fréquemment, en particulier lorsque de nombreux éléments dans listview défilent, en fait, elle est appelée chaque fois qu'un élément listview devient visible sur scroll .

ViewHolder Pattern empêchera findViewById() d'être appelé beaucoup de fois inutilement, en gardant les vues sur une référence statique, c'est un bon modèle pour économiser certaines ressources (en particulier lorsque vous devez référencer de nombreuses vues dans vos éléments listview) .

très bien dit par @RomainGuy

ViewHolder peut et doit également être utilisé pour stocker des structures de données temporaires afin d'éviter les allocations de mémoire dans getView (). Le ViewHolder contient un tampon de caractères pour éviter les allocations lors de l'obtention des données du curseur.

5
Bhavesh Patadiya