Il y a 7 ans -
Temps de lecture 8 minutes
Android – de ViewHolder à CustomView
Pré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) :
[java gutter= »true »]@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;
}[/java]
Les clés de cette architecture sont :
- ligne 3 : le
ViewHolder
qui ne sera créé que si la vue à recycler, passée en argument, estnull
. (ligne 5) (la variable d’instanceinflater
correspond au stockage du résultat deLayoutInflater.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éthodesetTag
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 :
[java gutter= »true »]private static class BookViewHolder {
final TextView titleTextView;
final ImageView coverImageView;
public BookViewHolder(TextView titleTextView, ImageView coverImageView) {
this.titleTextView = titleTextView;
this.coverImageView = coverImageView;
}
}[/java]
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
:
[java gutter= »true »]
<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>[/java]
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 :
[java gutter= »true »]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
}
}[/java]
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()
:
[java gutter= »true »]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;
}[/java]
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 :
[java gutter= »true »]public interface ItemView<T> {
void bindView(T o);
}[/java]
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 :
[java gutter= »true »]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;
}
}[/java]
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 à :
[java gutter= »true »]// 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);[/java]
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.
Commentaire
2 réponses pour " Android – de ViewHolder à CustomView "
Published by Secaf Épau , Il y a 7 ans
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.
Published by Benjamin Lacroix , Il y a 7 ans
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 ;).
Published by Jessica AL ARAYE , Il y a 7 ans
Interesting ! Thanks