Composants perso réutilisables sous Android

Je suis en train de faire une petite maquette pour un client et je me rends compte qu’il y’a, comme dans la plupart des applis d’ailleurs, des éléments qui sont répétés plein de fois, avec des faibles variations. Par ex, un écran avec plusieurs sections, divisées par des titres qui ont presque tout en commun: police, taille, background, layout… Y’a seulement une icône qui change sur le côté et le texte, évidemment.

Le but de cet article est donc de montrer comment on peut faire du code réutilisable dans un layout Android. Le scénario d’utilisation de ce modèle, c’est un super développeur qui va créer des composants perso et les donner à une équipe de développeurs, qui n’auront qu’à drag & drop le composant sur leur layout et à remplir les propriétés.

Et là je me dis, mais attends, Android, ils ont pensé à tout, non, avec leur modèles en XML? et bien oui…
C’est comme en Swing mais plus simple à utiliser. En fait, ca ressemble à du « bon » vieux VB 6!

Voilà comment faire, en 4 étapes:

1) créer le modèle en XML: moi je veux donc un titre de section sur lequel on peut cliquer donc j’ai mis un Button. Et j’ai juste une ImageView en plus pour l’icone, sur la gauche. Comme on peut le remarquer, je n’ai pas mis la balise « src » pour l’imageview. La source de l’ImageView sera remplie plus tard.
Bon, pour compliquer un peu les choses, j’ai rajouté un RelativeLayout (celui qui a l’id = panel) en dessous en visibility GONE. Je m’en servirai après pour rajouter des éléments à mon item générique.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:id="@+id/custom">
  <Button android:id="@+id/BTN"
  	android:layout_width="fill_parent"
  	android:layout_height="wrap_content" 
  	android:background="#FF0000"
  	android:text=""
  	android:textColor="@color/gris_bg"
  	android:layout_alignBaseline="@+id/IMG"
	android:layout_alignTop="@+id/IMG" 
	android:layout_alignBottom="@+id/IMG"
	android:layout_marginTop="12sp"
	android:layout_marginBottom="12sp"/>
	<RelativeLayout android:id="@+id/panel"
		android:layout_below="@id/BTN"
		android:visibility="gone"
		android:layout_width="fill_parent"
		android:layout_height="wrap_content" 
		>
	</RelativeLayout>
  <ImageView android:id="@+id/IMG"
  	android:layout_width="wrap_content"
  	android:layout_height="wrap_content" 
  	android:background="@android:color/transparent"
  	android:layout_alignParentTop="true"
  	android:layout_marginTop="20sp"
  	android:layout_marginLeft="20sp" 
  	android:layout_marginBottom="20sp"/>
</RelativeLayout>

note: mon fichier s’appelle customtitle.xml et se trouve dans res/layout

2) Je crée une définition des attributs pour mon modèle. Moi je veux juste changer le texte du Button et le drawable de l’imageview. Ce sont les « variables » que j’utiliserai dans mon composant perso. Donc j’ai juste besoin d’une String pour le texte du bouton et d’un champs Drawable pour la « source » de mon ImageView:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="stylableTitle">
        <attr name="txt" format="string"/>
        <attr name="android:drawable"/>            
    </declare-styleable>
</resources>

Ca c’est le contenu de mon fichier res/values/attrs.xml qui contient, comme son nom l’indique, une liste d’attributs.

3) Je crée une classe qui va faire la correspondance entre les attributs (étape 2) et mon layout modèle (étape 1). En gros, il s’agit de demander à Android de créer une instance de mon modèle XML, puis de remplir les champs text et src de mes composants TextView et ImageView.

public class CustomTitle extends RelativeLayout{
	private Button button;
	private ImageView img;

	public CustomTitle(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
		init(context,attrs);
	}
        // ce constructeur ne devrait jamais être appelé, car il n'a pas d'AttributeSet en paramètre.
	public CustomTitle(Context context) {
		super(context);
	}
	public CustomTitle(Context context, AttributeSet attrs) {
		super(context, attrs);
		init(context,attrs);
	}

	private void init(Context ctx, AttributeSet attrs){  

      // inflation du modèle "customtitle", et initialisation des composants Button et ImageView
      // on cherche le service Android pour instancier des vues
		LayoutInflater li = (LayoutInflater) ctx.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
      // on instancie notre vue customisée (celle créée dans l'étape 1, qui se trouve dans res/layout/customtitle)
		View v = li.inflate(R.layout.customtitle, null);
		button = (Button)v.findViewById(R.id.BTN);
		img = (ImageView)v.findViewById(R.id.IMG);
		addView(v);

      // Le modèle est chargé, on a plus qu'à l'initialiser avec les attributs qu'on a reçus en paramètre

	TypedArray a = ctx.obtainStyledAttributes(attrs, R.styleable.stylableTitle);

      // on obtient un TypedArray, une classe qui a plein de méthodes getString(int index),
      // getInteger(int index) (...) pour obtenir la valeur String, Integer (...) d'un attribut.

      // on vérifie que l'attribut "txt" n'est pas null: ce test est important! on verra par la suite pourquoi
	if(a.getString(R.styleable.stylableTitle_txt)!=null)
	        button.setText(a.getString(R.styleable.stylableTitle_txt));

      // et on recommence pour l'attribut "drawable"
	if(a.getDrawable(R.styleable.stylableTitle_android_drawable)!=null)
	        img.setBackgroundDrawable(a.getDrawable(R.styleable.stylableTitle_android_drawable));
      
      // on recycle, c'est pour sauver mère nature
	        a.recycle();
	}
}

Ici, j’ai simplement généré une classe qui étend un Layout (j’ai choisi RelativeLayout), en générant bien les constructeurs qui reçoivent un AttributeSet! Et j’appelle une méthode init() qui va « inflater » mon layout générique, récupérer les ID’s du button et de l’imageView. Le addView(v) permet d’ajouter la vue « inflatée » à « this », c’est à dire au RelativeLayout.

Dans un deuxième temps, je vais chercher dans les attributs les valeurs que je veux mettre dans chacun de mes composants. On reçoit en fait en paramètre un AttributeSet (attrs), qui contient une collection d’attributs. Il suffit d’extraire de cette liste tous les attributs qui nous intéressent, c’est à dire ceux qu’on a défini dans le fichier attrs.xml sous le nom « stylableTitle ». C’est fait avec la méthode obtainStyledAttributes.

Une fois qu’on a bien trouvé les attributs qu’on veut, on fait des setText() et setBackgroundDrawable() avec les valeurs des attributs, pour initialiser le modèle.

note: les valeurs R.styleable.* sont générées automatiquement

4) A ce stade, c’est presque fini, il ne reste plus qu’à utiliser ce qu’on a fait!
Dans un fichier de layout, il suffit d’écrire:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" 
	xmlns:app="http://schemas.android.com/apk/res/com.test"
	android:layout_width="match_parent"
	android:layout_height="wrap_content"
  <com.test.views.CustomTitle 
  	android:layout_width="match_parent"
  	android:layout_height="wrap_content"
  	android:drawable="@drawable/l_icone_de_mon_titre"
  	app:txt="le texte de mon titre"
  />
</RelativeLayout>

Ici, il y’a plusieurs subtilités, mais il faut d’abord regarder le principal: j’ai inséré une balise com.test.views.CustomTitle. Android va, comme pour toutes les autres balises de base, aller instancier la classe java qui correspond (donc ici, la notre qu’on a créé dans l’étape 2). Et j’ai rajouté à cette balise quelques attributs, pour la personnaliser un peu. layout_width et height, on connait. Je lui ai mis un attribut android:drawable, qui sera donc passé dans le constructeur public CustomTitle(Context context, AttributeSet attrs) {}, qui lui même va appeler la méthode init(), et on ira chercher l’attribut drawable pour le mettre en background de l’imageview du modèle, comme expliqué précédemment.

  • Une petite subtilité, c’est l’utilisation de la balise « app:txt ». En fait, pour l’exercice, j’ai exprès mis dans ma feuille d’attributs un nom de balise inconnu par Android. Il n’y a pas de balise android:txt! Pour que cette balise puisse être utilisée, j’ai donc rajouté le namespace de mon projet sur le composant parent.

    C’est la ligne xmlns:app= »http://schemas.android.com/apk/res/com.test »

    Elle indique que pour toutes les balises « app:XXXX », on cherche dans le package « com.test ». D’ailleurs, comme android:txt n’existe pas, il faut aussi lui préciser son type quand on la déclare ( ). Pour info, les formats disponibles sont:

    Reference, String, Color, Dimension, Boolean, Integer, Float, Fraction, Enum, Flag

    A ma connaissance, on ne peut pas ajouter de type de format! J’imaginais déjà un pattern où on donne directement un objet métier dans un attribut, une formation par exemple, et il remplit tout seul chacun des champs (le titre de la formation, l’url etc). On devra donc se contenter de faire des balises app:titreFormation, app:url, pour chacun des champs qu’on veut remplir.
  • Et comme il existe déjà une balise « android:drawable » qui contient un drawable dans le package « android », pas besoin de redéfinir celui là! J’aurais bien sur pu utiliser « android:text » ou même « android:src » pour afficher mon texte sur mon button. Mais ça n’aurait pas été aussi drôle, non?

    Ce système permet donc d’ajouter autant d’attributs qu’on veut, avec des noms intuitifs. Par exemple, on peut très bien imaginer de compliquer un peu le modèle en faisant une page entière qui aurait des attributs « body », « title », ou même une URL sur laquelle on irait chercher des infos.
  • Une autre subtilité, c’est qu’on peut très bien oublier un des attributs, ou lui donner une valeur qui n’a pas de sens. C’est pourquoi il vaut toujours mieux tester au niveau de la classe java la valeur des attributs. Pour le développeur qui va utiliser le composant, il est tout à fait possible de ne pas renseigner de valeur pour l’attribut « txt », ou même de ne pas écrire cet attribut du tout! Comme chacun sait, écrire setText(null) n’est pas recommandé!

Attention cependant, systématiquement tout faire passer dans les attributs n’est pas une bonne solution. On en perdrait l’intérêt original de ce procédé: généraliser ce qui est généralisable.
A l’utilisation, l’intérêt pour le développeur se voit cependant très rapidement. Si un élément se répète de nombreuses fois, éventuellement sur plusieurs pages, on n’a besoin de le modifier qu’à un seul endroit lorsqu’untel nous dit que « finalement, je veux le fonds en bleu et l’image alignée à droite »!

Et la prochaine fois, j’expliquerai pourquoi mon modèle n’aurait pas du être un <RelativeLayout>, mais plutôt un <merge> ! Quoiqu’en fait, il suffit de lire cet article ici.

En revanche, la prochaine fois, j’expliquerai comment savoir si on doit faire un <include> ou un composant perso comme expliqué ci dessus.