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 :
<?
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 :
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 :
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) :
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 :
<?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 :
<?
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.
<?
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 :
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 :
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 :
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 :
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.
this
.setAdapter
(new
ArrayAdapter<
String>
(context,android.R.layout.select_dialog_item, datas));
Notre CustomAutoComplete ressemble maintenant à ça :
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 :
@
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.
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 ) :
m =
DataSource.class
.getMethod
(source, new
Class[] {
Context.class
, String.class
, Integer.TYPE}
);
On instancie la classe
DataSource
, pour l'appel
Invoke
:
DataSource ds =
new
DataSource
();
Et on invoke la méthode avec ses paramètres :
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 !!!