0. Introduction

Vous le savez, les fichiers XML de layout sont bien pratiques pour créer de jolis formulaires, sans perdre de temps à écrire du code pour instancier chaque contrôle.

Mais, pour moi qui suis un éternel fainéant, il reste encore trop de code rébarbatif à écrire qui pollue inutilement le code « utile ».

Par exemple, dans un formulaire, l'utilisateur vous sera très reconnaissant si vous lui fournissez des AutoCompleteTextView au lieu des basiques EditText. Mais pour chaque AutoComplete que vous allez placer dans votre formulaire, il faudra, dans le code de l'activity, lui affecter un adapter comme source de données.

Avec les Custom Properties, nous allons pouvoir créer notre propre AutoComplete qui ne nécessitera aucun code pour aller chercher les données.

I. Les bases

Commençons par voir rapidement le fonctionnement classique d'un AutoCompleteTextView : pour cela, nous créons un formulaire avec un seul champ qui sera notre AutoCompleteTextView ; pour l'instant, notre source de données sera un tableau de String mais il faut garder à l'esprit que le tutoriel n'a d'intérêt que pour des données dynamiques (typiquement, issues d'une table SQLite).

Un layout, tout d'abord :

 
Sélectionnez
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <AutoCompleteTextView
        android:id="@+id/autoCompleteExample"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"/>
</LinearLayout>


Notre activity, basique, aura a minima le code suivant :

 
Sélectionnez
package com.shebuterne.tutorials.autocomplete;

import android.app.Activity;
import android.os.Bundle;
import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView;
import com.shebuterne.tutorials.R;

public class AutoComplete1 extends Activity{

    @Override
    protected void onCreate(Bundle savedInstanceState){
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_autocomplete);

        // On ne veut plus écrire ces lignes de code !!! 
        AutoCompleteTextView autoCompleteExempe = (AutoCompleteTextView)findViewById(R.id.autoCompleteExempleBasic);
        ArrayAdapter<String> autoCompleteAdapter = new ArrayAdapter<String>(this, android.R.layout.select_dialog_item, DataSource.GetChiffres());
        autoCompleteExempe.setAdapter(autoCompleteAdapter);
    }
}

La classe fournissant les données est la suivante :

 
Sélectionnez
package com.shebuterne.tutorials.autocomplete;

public class DataSource {
    public static String[] GetChiffres(){
            return new String[]{"un","deux","trois","quatre","cinq","six","sept","huit","neuf","dix"};
    }
}

J'insiste bien sur le fait que dans la vrai vie, la méthode GetChiffres devra aller chercher ses données dans une table SQLite (et donc avoir quelques arguments en entrée).

II. Et avec les customs Properties

Ce que l'on va faire pour rendre tout cela un peu plus efficace, c'est indiquer directement dans le fichier XML du layout la source de données.

Pour cela, la première chose à faire est de créer notre propre AutoCompleteTextView que l'on pourra ensuite customiser.

Un premier jet donne ceci (fichier CustomAutoComplete.java)   :

other
Sélectionnez
package com.shebuterne.tutorials.autocomplete;

import com.shebuterne.tutorials.R;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView;

public class CustomAutoComplete extends AutoCompleteTextView{

    public CustomAutoComplete(Context context) {
        super(context);
     }

    public CustomAutoComplete(Context context, AttributeSet attr) {
        super(context, attr);
    }
}
  • C'est le second constructeur qui est appelé au moment du inflate .

Nous avons créé notre CustomAutoComplete qui peut être directement utilisé dans le layout de départ :

other
Sélectionnez
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <com.shebuterne.tutorials.autocomplete.CustomAutoComplete
        android:id="@+id/autoCompleteExemple"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"/>
</LinearLayout>
  • La seule chose qui ait changée, c'est le contrôle AutoComplete qui n'est plus le AutoCompleteTextview mais notre CustomAutoComplete.
  • Le nom du contrôle est constitué du nom du package et de la classe du contrôle.

C'est dans ce fichier que nous allons indiquer la source de données.

Commençons par déclarer une customProperties : cela se fait simplement en rajoutant un fichier attr.xml (ou en le complétant si vous l'utilisez déjà pour autre chose).

Le fichier attr.xml se place dans le répertoire values et, ici, ne comportera qu'un seul élément :

 
Sélectionnez
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="CustomAutoComplete">
        <attr name="adapterSource" format="string"/>
    </declare-styleable>
</resources>
  • Nous déclarons un nouvel attribut qui se nomme adapterSource.

Il faut ensuite le reporter dans la balise de notre CustomAutoComplete dans le fichier layout.

 
Sélectionnez
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:shebuterne="http://schemas.android.com/apk/res/com.shebuterne.tutorials"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <com.shebuterne.tutorials.autocomplete.CustomAutoComplete
        android:id="@+id/autoCompleteExemple"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        shebuterne:adapterSource="GetChiffres"/>
</LinearLayout>

Notez les trois points suivants :

  • nous avons rajouté un espace de nom pour pouvoir utiliser un alias dans la balise CustomAutoComplete ;
  • l'espace de nom est celui du nom du package généré par le compilateur (celui qui contient toutes les ressources « R » propres à notre projet) ;
  • nous avons positionné notre attribut « adapterSource » et sa valeur est le nom de la méthode que nous voulons appeler comme source de données.


Et maintenant, il faut modifier notre class CustomAutoComplete pour qu'elle prenne en compte ce nouvel attribut.


La première chose à faire est de récupérer la valeur donnée à notre attribut :

 
Sélectionnez
String source =null;
TypedArray customAttributes = context.obtainStyledAttributes(attr, R.styleable.CustomAutoComplete);

if(customAttributes!=null)
    source = customAttributes.getString(R.styleable.CustomAutoComplete_adapterSource);

customAttributes.recycle();
  • Notez bien que R.styleable.CustomAutoComplete_adapterSource a été généré par le compilateur.

Si tout se passe bien, à l'exécution, source contiendra la chaîne GetChiffres.

La seconde chose à faire est d'utiliser la réflexion pour appeler la méthode. Ce qui donne cela :

 
Sélectionnez
Method m=null;
String[] datas =null;

try {
    m = DataSource.class.getMethod(source);
    datas = (String[]) m.invoke(DataSource.class, null);
} 
catch (NoSuchMethodException e) {
            e.printStackTrace();
} catch (IllegalArgumentException e) {
            e.printStackTrace();
} catch (IllegalAccessException e) {
            e.printStackTrace();
} catch (InvocationTargetException e) {
            e.printStackTrace();
}


En fait, il n'y a que deux lignes dans ce bout de code :

 
Sélectionnez
Method m = DataSource.class.getMethod(source);

permet de récupérer un « pointeur » sur la méthode que l'on veut exécuter. Ici, la méthode est static et ne prend pas d'arguments (on verra plus loin le code nécessaire pour passer des paramètres).


Et :

 
Sélectionnez
datas = (String[]) m.invoke(DataSource.class, null);

nous permet d'appeler la méthode et de récupérer les données retournées.

Et enfin, nous pouvons fournir à notre AutoComplete l'adapter qu'il attend.

 
Sélectionnez
this.setAdapter(new ArrayAdapter<String>(context,android.R.layout.select_dialog_item, datas));


Notre CustomAutoComplete ressemble maintenant à ça :

 
Sélectionnez
package com.shebuterne.tutorials.autocomplete;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import com.shebuterne.tutorials.R;

import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView;

public class CustomAutoComplete extends AutoCompleteTextView{

    public CustomAutoComplete(Context context) {
        super(context);
     }

    public CustomAutoComplete(Context context, AttributeSet attr) {
        super(context, attr);
        String source =null;
        TypedArray customAttributes = context.obtainStyledAttributes(attr, R.styleable.CustomAutoComplete);
        if(customAttributes!=null)
            source = customAttributes.getString(R.styleable.CustomAutoComplete_adapterSource);
        customAttributes.recycle();
       
        if(source==null)
            return;
        
        Method m=null;
        String[] datas =null;
     
        try {
            m = DataSource.class.getMethod(source);
            datas = (String[]) m.invoke(DataSource.class, null);
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }

        if(datas==null)
            return;
        
        this.setAdapter(new ArrayAdapter<String>(context,android.R.layout.select_dialog_item, datas));
     }
    
    
}


On supprime le code désormais inutile de l'activity :

 
Sélectionnez
@Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_autocomplete);
        // On ne s'occupe plus de l'AutoComplete et de son Adapter !!!
    }


L'essentiel est terminé, il ne reste plus qu'à voir ce qui change, si notre méthode nécessite des arguments et si elle n'est pas statique.

Imaginons que GetChiffres prenne en paramètre le context, une chaîne et un entier (par exemple, pour aller chercher les données dans une base SQLite avec un filtre et un nombre maximum de données à retourner), et qu'elle ne soit pas statique.

 
Sélectionnez
public String[] GetChiffres(Context context, String aParameter, int anotherOne)
{
    /*
     .. normalement, ici, on trouve
     ... du code qui utilise le context, ainsi que les deux paramètres.
     */
    return new String[]{"un","deux","trois","quatre","cinq","six","sept","huit","neuf","dix"};
}


Le code impacté ne sera que celui utilisé lors de la réflexion.

On indique dans getMethod , la signature de la méthode que l'on souhaite appeler (notez bien comment on gère le cas de l' integer ) :

 
Sélectionnez
m = DataSource.class.getMethod(source, new Class[] {Context.class, String.class, Integer.TYPE});


On instancie la classe DataSource , pour l'appel Invoke  :

 
Sélectionnez
DataSource ds = new DataSource();


Et on invoke la méthode avec ses paramètres :

 
Sélectionnez
datas = (String[]) m.invoke(ds, new Object[]{context,"someData",3});

On voit bien ici le fonctionnement d'invoke : le premier paramètre, l'objet qui portera l'appel à la méthode est dans ce cas une instance de la classe alors que lorsque la méthode était statique, c'est la classe elle-même qui portait l'appel.

Voilà, tout est terminé ! Nous disposons maintenant d'un AutoComplete pratique, idéal pour les fainéants.

III. Conclusion

Ce petit exemple ne s'applique évidemment pas uniquement aux autoCompleteTextView : l'utilisation du même mécanisme peut être étendue à tous les contrôles que vous utilisez. Les ListView, notamment, seront des candidates parfaites à ce genre d'astuce.

Et n'oubliez pas, chaque ligne de code que vous n'écrivez pas est un bogue potentiel en moins !!!

IV. Remerciements

Merci à tous ceux qui m'ont aidé pour cette première publication, en particuler f-leb qui s'est chargé de la correction, et à _Max_, dont les interventions ont toujours été pertinentes et décisives.