lundi 4 juillet 2011

L'Adapter à Terre

Fini la glande !

Depuis mon dernier article, j'ai pas mal aguerri mon skill en développement Android, ce qui me permet de vous filer quelques tuyaux. Voilà une petite introduction aux Adapters dont l'usage est plutôt fréquent lorsqu'on bosse sous Android.

Qu'est ce que c'est ?

Un adapter est un design pattern permettant de lier 2 classes qui ne sont a priori pas compatibles entre elles. Dans Android, cela sert principalement à lier des données stockées sous une forme quelconque à une vue. Cela a l'avantage de rendre le code des composants graphiques totalement indépendant du type de données à afficher. Pour prendre un exemple concret, c'est un adapter qui permet à une ListView de piocher les données à afficher dans le Cursor retourné par une requete DB, ou dans le contenu d'une List Java.

Mise en œuvre

Nous allons créer un adapter permettant d'afficher des contacts stockés dans une List au sein d'une ListView. Un contact sera stocké dans la classe Contact.

Mettons en place le layout d'un élément de notre liste. Nous allons afficher un contact par son nom et prénom, un TextView est donc approprié :
<?xml version="1.0" encoding="utf-8"?>
<TextView
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:id="@+id/contactName"
  android:layout_width="fill_parent"
  android:layout_height="wrap_content"/>
Ensuite l'Adapter en lui-même. On créé une classe ContactAdapter qui hérite de ArrayAdapter, qui est une implémentation spécialisée pour les tableaux. Notre constructeur va prendre 3 arguments. Le contexte de l'Activity, l'identifiant du layout permettant d'afficher un contact (soit R.id.contactName), et notre List de contacts. On appelle le constructeur de la super classe avec ces éléments. Enfin, on stocke notre List en temps que variable d'instance car nous aurons besoin d'y accéder plus tard.
public class ContactAdapter extends ArrayAdapter<Contact> {
  
  private List<Contact> contactList;
  
  public ContactAdapter(Context context, int textViewResourceId,
                        List<Contact> contactList) {
    super(context, textViewResourceId, contactList);
    this.contactList = contactList;
  }
}
Il nous faut maintenant surcharger la méthode getView, qui permet de retourner une vue permettant l'affichage d'un contact dans notre ListView:
public View getView(int position, View convertView,
                    ViewGroup parent) {
  View v = convertView;
  if (v == null) {
    LayoutInflater vi = (LayoutInflater) getContext().
             getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    v = vi.inflate(R.layout.contact_entry, null);
  }
  Contact contact = contactList.get(position);
  if (contact != null) {
    TextView name = (TextView) v.findViewById(R.id.contactName);
    if (name != null) {
      name.setText(contact.getName());
    }
  }
  return v;
}
Un des principes fondamentaux à appliquer dans cette méthode est la réutilisation des vues. En effet, instancier une vue est un mécanisme très couteux en ressource, alors que réutiliser une vue en changeant son contenu (ici avec la méthode setText()) est très rapide. De plus, avec cette technique, seules les vues effectivement à l'écran sont en mémoire. Cela se traduit dans le code par "si une vue est nulle, je l'instancie, sinon je la réutilise".

Utilisons maintenant notre super Adapter ! Pour cela, il faut appeler la méthode setAdapter() sur notre ListView comme ceci :
mContactList.setAdapter(new ContactAdapter(this, R.layout.contact_entry, contactList));
Avec contactList défini comme champ dans notre Activity :
private final List<Contact> contactList = new ArrayList<Contact>();
On notera l'utilisation ici du mot-clef final. En effet, j'ai constaté que si contactList est réinstanciée après la création de l'adapter, celui-ci ne répondra plus correctement. Final permet d'éviter cela en interdisant la modification de notre référence Java.

Dernier point, lorsque notre List est modifiée, il faut appeler la méthode suivante pour forcer le rafraichissement de notre ListView :
((ContactAdapter) mContactList.getAdapter()).notifyDataSetChanged();