Publié par
Il y a 2 années · 8 minutes · Android, Mobile

Android – de ViewHolder à CustomView

android-logoPrésenter des données sous forme de liste sur Android n’a pas toujours été aussi simple qu’avec une RecyclerView. Pour rappel, jusqu’à Android L (Api 21) nous devions utiliser l’un des composants (widget) les plus complexes : une ListView.

Poussé par Google et beaucoup utilisé par les développeurs, le patron de conception ViewHolder est devenu l’architecture utilisée pour obtenir un defilement rapide et fluide. Pour information, la RecyclerView et  la classe RecyclerView.ViewHolder sont directement liés à ces travaux.

Dans cet article nous verrons comment aller plus loin que le patron de conception ViewHolder.

L’objectif est de proposer une façon plus élégante et plus pérenne pour construire des listes de vues efficaces en séparant les responsabilités. Nous aborderons les CustomView et construirons un Adapter agnostique au niveau de la vue qui défile et des objets manipulés.

Prenons l’exemple d’une bibliothèque de livre (Library, Book, etc.) et essayons d’afficher tous les livres sous la forme d’une liste qui peut défiler.

ViewHolder

À titre de comparaison, voyons d’abord à quoi ressemble le patron de conception ViewHolder.

Les objets du modèle sont simplifiés et le contenu de certaines méthodes n’est volontairement pas présenté dans cet article : n’hésitez pas à regarder directement le dépôt pour les détails.

L’Adapter permet de recycler les vues : dans celui-ci, la méthode la plus intéressante est getView() qui permet de construire la vue (la première fois) puis de compléter les widgets la composant (les autres fois) :

@Override
public View getView(int position, View convertView, ViewGroup parent) {
   BookViewHolder bookViewHolder;
   if (convertView == null) {
       convertView = inflater.inflate(R.layout.view_holder_book, parent, false);
       TextView titleTextView = (TextView) convertView.findViewById(R.id.titleTextView);
       ImageView coverImageView = (ImageView) convertView.findViewById(R.id.coverImageView);
       convertView.setTag(new BookViewHolder(nameTextView, coverImageView));
   }
   Book book = books.get(position);
   bookViewHolder = (BookViewHolder) convertView.getTag();
   bookViewHolder.titleTextView.setText(book.getTitle());
   return convertView;
}

Les clés de cette architecture sont :

  • ligne 3 : le ViewHolder qui ne sera créé que si la vue à recycler, passée en argument, est null. (ligne 5) (la variable d’instance inflater correspond au stockage du résultat de LayoutInflater.inflate(context) lors de la construction de l’Adapter) ;
  • ligne 6 et 7 : les widgets de la vue ne sont récupérés qu’une seule fois (findViewById est couteux en performance) ;
  • ligne 8 : le ViewHolder est ajouté à la vue grâce à la méthode setTag pour être récupéré plus tard ;
  • ligne 11 : récupération du ViewHolder ;
  • ligne 12 et 13 : complétion des widgets avec les données du Book.

Puis vient la définition du ViewHolder correspondant :

private static class BookViewHolder {
   final TextView titleTextView;
   final ImageView coverImageView;

   public BookViewHolder(TextView titleTextView, ImageView coverImageView) {
      this.titleTextView = titleTextView;
      this.coverImageView = coverImageView;
   }
}

L’objectif principal est de conserver une référence sur les widgets pour ne pas faire appel à findViewById à chaque fois (défilement).
À ce niveau-là, le patron de conception ViewHolder est correctement utilisé, plus de détails sur le dépôt sous le package fr.blacroix.viewholder
Comme précisé plus haut, l’objectif est d’aller plus loin… Évoluons donc vers une CustomView.

ViewHolder vers CustomView

Créer une CustomView revient à créer une vue (layout + représentation en classe Java) qui étend une vue existante du système Android (LinearLayout, RelativeLayout, CardView, TextView, etc.).

Dans l’article nous créerons un Custom LinearLayout horizontal contenant une ImageView et une TextView : l’image et le nom du livre.

Le XML correspondant à cette vue est sensiblement identique au layout utilisé dans la partie ViewHolder :

<fr.blacroix.customview.BookItemView 
  xmlns:android="http://schemas.android.com/apk/res/android" 
  xmlns:tools="http://schemas.android.com/tools" 
  android:layout_width="match_parent" android:layout_height="100dp" 
  android:orientation="horizontal" android:padding="10dp">

  <ImageView 
    android:id="@+id/coverImageView" 
    android:layout_width="100dp" 
    android:layout_height="100dp" 
    android:padding="10dp"
    android:src="@drawable/ic_launcher" 
    tools:ignore="ContentDescription"/>

  <TextView 
    android:id="@+id/titleTextView" 
    android:layout_width="wrap_content" 
    android:layout_height="wrap_content" 
    android:layout_gravity="center" 
    android:layout_weight="1"
    android:padding="10dp" 
    tools:text="Henri Potier à l'école des sorciers"/>

</fr.blacroix.customview.BookItemView>

La définition d’une CustomView au niveau du layout se fait par référencement de la classe comme élément root du layout ici : fr.blacroix.customview.BookItemView
La suite consiste à définir la classe BookItemView correspondante. Encore une fois, certaines parties sont volontairement omises :

public class BookItemView extends LinearLayout {

    private TextView titleTextView;
    private ImageView coverImageView;
 
 // ... Constructors

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        titleTextView = (TextView) findViewById(R.id.titleTextView);
        coverImageView = (ImageView) findViewById(R.id.coverImageView);
    }

    public void bindView(Book book) {
        titleTextView.setText(book.getTitle());
        // Use Picasso or Glide to download book.getCover() into coverImageView
    }
}

Le point à noter est que notre CustomView étend LinearLayout (nous avons créé un custom ViewGroup qui étend un LinearLayout).
Dans la méthode onFinishInflate() nous récupérons et stockons en variable d’instance les widgets qui composent notre ViewGroup.
La méthode bind() permet de compléter les widgets avec les données du Book.

Une fois la CustomView redéfinie nous pouvons l’utiliser dans l’Adapter, voyons à nouveau la méthode getView() :

public View getView(int position, View convertView, ViewGroup parent) {
    BookItemView bookItemView;
    if (convertView == null) {
        bookItemView = (BookItemView) inflater.inflate(R.layout.custom_view_book, parent, false);
    } else {
        bookItemView = (BookItemView) convertView;
    }
    bookItemView.bindView(getItem(position));
    return bookItemView;
}

La méthode getView() est modifiée pour faire appel à la CustomView BookItemView. Une fois le layout gonflé il est possible de convertir l’objet en BookItemView et d’appeler directement la méthode bindView() pour remplir la vue. Comme pour le ViewHolder la performance est assurée car le layout n’est gonflé qu’une fois et les widgets ne sont récupérés qu’à la construction de la classe.
À partir de cette architecture il est aisé de séparer l’Adapter et la CustomView. Il y a moins de boilerplate dans l’Adapter et toute la gestion de la vue se fait directement dans la classe correspondante au layout.

Voilà, nous avons transformé le patron de conception ViewHolder en CustomView. À partir de là, il est possible d’aller encore un peu plus loin :) !
Dépôt et sources complètes dans le package fr.blacroix.customview
La suite de l’article propose un procédé pour ne pas avoir un Adapter par liste, mais ré-utiliser le même…

Generic Adapter

Dans nos applications il y a souvent plusieurs listes, donc plusieurs Adapter alors que le code se ressemble assez souvent.
Voyons comment supprimer ce boilerplate en utilisant le CustomView et un Adapter générique.

D’abord, voyons la CustomView, il faut rendre génériques nos CustomView en implémentant une interface :

public interface ItemView<T> {
    void bindView(T o);
}

Nous passons un type à notre interface qui correspond à l’objet qui défilera (Book dans notre cas).
Comme dans la partie CustomView, la méthode bindView() permet de compléter les widgets. La CustomView implémente cette interface.

Le job complexe revient à l’Adapter qui permettra de gérer n’importe quelle vue qui implémente l’interface ItemView et n’importe quel type de données :

public class Adapter<T, V extends ItemView<T>> extends BaseAdapter {
    private final LayoutInflater inflater;
    private final List<T> data;
    private final int viewId;

    public Adapter(LayoutInflater inflater, List<T> data, int viewId) {
        this.inflater = inflater;
        this.data = data;
        this.viewId = viewId;
    }

    @Override
    public int getCount() {...}

    @Override
    public T getItem(int position) {...}

    @Override
    public long getItemId(int position) {...}

    @SuppressWarnings("unchecked")
    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        V view;
        if (convertView == null) {
            view = (V) inflater.inflate(viewId, parent, false);
        } else {
            view = (V) convertView;
        }
        view.bindView(getItem(position));
        return (View) view;
    }
}

La classe générique est paramétrée en fonction des objets T manipulés, de la vue V qui étend l’interface ItemView qui elle-même connait le type de données manipulées T.
A noter tout de même qu’il y a une erreur Lint aux lignes 26 et 28 du fait du cast non vérifié et qu’il est impératif que la classe qui implémente l’interface étende bien une vue ;)

Utiliser cet Adapter se résume à :

// Liste de livres
Adapter<Book, BookItemView> bookAdapter = new Adapter<>(inflater, books, R.layout.item_view_book);
// Liste d'auteurs
Adapter<Author, AuthorItemView> authorAdapter = new Adapter<>(inflater, authors, R.layout.item_view_author);

Dépôt et sources complètes dans le package fr.blacroix.generic.

Conclusion

Nous avons abordé la patron de conception ViewHolder, puis comment aller plus loin avec les CustomView et ensuite comment limiter le boilerplate de l’Adapter en le rendant générique.
Même si la RecyclerView reprend pas mal de ces concepts, la ListView est toujours un composant rencontré en production.
Parfois, il n’est pas nécessaire de tout transformer en RecyclerView mais seulement d’adapter le code existant vers une architecture plus performante.

Benjamin Lacroix
Benjamin Lacroix est lead développeur et manager chez Xebia. Il est également formateur au sein de Xebia Training .

2 réflexions au sujet de « Android – de ViewHolder à CustomView »

  1. Publié par Secaf Épau, Il y a 2 années

    Pas très godeux tout ça… La RecyclerView est là depuis 1 an et demi et à moins d’être un complet débutant on a tous déjà fait ça depuis longtemps. Depuis quand la ListView est plus complexe que la RecyclerView ?

    PS/ Le XML est illisible.

  2. Publié par Benjamin Lacroix, Il y a 2 années

    Bonjour,
    La ListView est plus complexe car le développeur doit utiliser le patron de conception ViewHolder pour que la liste soit performante. Tandis que la RecyclerView accompagne l’utilisation du patron de conception.
    Nous allons indenter le XML, merci ;).

  3. Publié par Jessica AL ARAYE, Il y a 1 année

    Interesting ! Thanks

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *