samedi 23 juillet 2011

Création de vues sur Android

     Sur Android les composants graphiques sont appelés des « vues ». Pour des contraintes de simplicité et d'ergonomie leur choix a été optimisé - par exemple le TreeView a été enlevé et le Carousel ajouté. La plateforme vient aussi avec un framework très riche qui facilite la construction d'interfaces graphiques intuitives. On aura toujours à structurer les vues par le biais des layout, comme dans les framework Java.

Il y a deux méthodes pour faire des IHM :
    • la première – qu'on peut appeler «Méthode Java » - sans doute la plus accessible aux développeurs java - identique à celle de Swing - consiste à déclarer et à instancier toutes les vues dans des classes Java. L'inconvénient majeur de cette méthode est d'avoir un mélange entre le code statique relatif à l'IHM et le code métier, le tout dans des classes dont la longueur frôle le millier de lignes.
    • la seconde – « Méthode Android » - qui est la plus originale - corrige les défauts de la première en rendant possible la définition d'une partie de l'IHM dans des fichiers XML, communément appelés fichiers « ressources » (res/) . D'autres technologies avant Android comme XAML de Microsoft et Adobe de Flex ont intégré le XML dans la création d'interfaces graphiques. L'avantage principal ici est qu'on obtient un code plus aéré et plus maintenable.

Il existe d'autres raisons d'utiliser la seconde méthode :
    • elle permet la réutilisation de composants graphiques
    • on peut adapter l'IHM à différentes configurations du terminal mobile, résolution de l'écran, disposition paysage/portrait, langue, ...
    • elle offre des concepts - comme les styles et les thèmes – qu'il est impossible d'utiliser avec l'API Java.
    • elle est la mieux documentée et présente plus d'exemples de code …
S'il y a quelque chose à retenir ici c'est qu'il existe très peu de cas qui nécessitent d'instancier une vue avec la « Méthode Java ». Sur le site officiel Google même conseille d'externaliser toutes les ressources afin de pouvoir faciliter la maintenance de l'application.
Un exemple simple pour illustrer quelques fonctionnalités :
    • déclaration du squelette de l'Activité dans le layout main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"   
    android:layout_height="fill_parent"
    android:layout_gravity="center">
            
        <Button
            android:text="Validate"
            android:id="@+id/btnValidate"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_margin="10dip"/>

        <Button
            android:text="Next > "
            android:id="@+id/btnNext"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_margin="10dip"/>               
   
</LinearLayout>
    • code Java
public class MainView extends Activity {
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);       
        setContentView(R.layout.main);

      // On récupère le bouton déclaré dans layout.xml       
        Button btnNext = (Button) findViewById(R.id.btnNext);

      // On associé un événement au bouton
        btnNext.setOnClickListener(new OnClickListener(){           
            public void onClick(View view) {
                //
            }           
        });
       
        Button btnValidate = (Button) findViewById(R.id.btnValidate);
        btnValidate.setOnClickListener(new OnClickListener(){
            public void onClick(View view) {
                //
            }           
        });       
    }
}
Quand on fait setContentView() le système génère la structure du layout dont la référence est passée en paramètre – en affichant toutes ses vues et en rendant les références de celles-ci disponibles dans le code Java par findViewById(). Ce processus de génération d'une vue à partir d'un fichier XML est appelé « Layout inflation ». 

Strings.xml
On peut renseigner le label d'une vue directement dans le code en utilisant la propriété setText() ou dans le fichier xml du layout avec l'attribut text. Mais la meilleure manière est d'utiliser strings.xml. On gagne dans l'externalisation des constantes et surtout dans l'internationalisation. En effet on peut disposer de différentes versions de ce fichier (strings-fr.xml, strings-eng.xml, ...) dans «res/values» – selon les différentes langues prises en compte dans l'application - et laisser le système charger dynamiquement celui qui correspond à la configuration du mobile.
Dans l'exemple précèdent les labels des boutons sont écrits dans le layout. Pour les externaliser il suffira de créer des entrées dans strings.xml …
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!--- ... -->
    <string name="btnValidateLabel">Validate</string>
    <string name="btnNextLabel">Next </string>
</resources>
… et de les référencer dans le layout.
<Button 
    android:text="@string/btnValidateLabel"
    android:id="@+id/btnValidate"
    
/>

<Button 
    android:text="@string/btnNextLabel"
    android:id="@+id/btnNext"
    
/>
Include
La balise «include» est utile dans des cas où l'on retrouve plusieurs fois la même portion d'un composant graphique dans différentes activités - un titre par exemple – ou quand on veut découper un layout en plusieurs parties.

Exemple : Inclure un autre layout title.xml dans le layout principal main.xml.
On commence par créer une entrée supplémentaire dans strings.xml pour le label affiché dans le titre …
<string name="titleLabel">Include sample</string>
… puis créer title.xml … 
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 
        xmlns:android="http://schemas.android.com/apk/res/android"
           android:layout_width="fill_parent"
           android:layout_height="wrap_content"
           android:orientation="horizontal"
           android:gravity ="center">  
       
           <TextView
               android:id="@+id/lblTitle"
               android:text="@string/titleLabel"
               android:layout_width="wrap_content"
               android:layout_height="wrap_content"
               android:gravity="center"/>
</LinearLayout>   
… et enfin l'inclusion dans main.xml
<include android:id="@+id/title" layout="@layout/title"/>

LayoutInflater
Il existe des cas où des vues personnalisées doivent être créées de manière dynamique, parce qu'on en connait pas le nombre; par exemple quand on doit remplir un TableLayout (une table) dont le nombre de RowLayout (lignes) n'est pas connu à l'avance. La première solution est de les instancier dans une boucle avec du code Java mais la méthode la plus élégante est de faire soi-même le «Layout Inflation». Le principe est de définir la vue personnalisée dans un fichier XML et de la générer dans le code.

Exemple : Créer N RowLayout dans un TableLayout
Dans le layout principal on ne définit que l'entête du TableLayout …
<TableLayout
        android:id="@+id/tableLayout"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_margin="5dip"
        android:gravity="center_vertical">
        

        <TableRow
            android:layout_width="fill_parent"
            android:layout_height="wrap_content">
                        
            <TextView
                android:text="@string/firstNameLabel"
                android:layout_width="100dip"
                android:layout_height="wrap_content"/>                                
            
            <TextView
                android:text="@string/lastNameLabel"
                android:layout_width="100dip"
                android:layout_height="wrap_content"/>
                <!-- ... -->                                 
        </TableRow>        
            
</TableLayout>
… dans row_layout.xml ...
<?xml version="1.0" encoding="utf-8"?>
<TableRow
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    >
                        
    <TextView
        android:id="@+id/txtFirstName"
        android:layout_width="100dip"
        android:layout_height="wrap_content"/>                                
            
    <TextView
        android:id="@+id/txtLastName"
        android:layout_width="100dip"
        android:layout_height="wrap_content"/>                                                            
</TableRow> 
… dans le code java on initialise notre « Inflater ...
LayoutInflater  inflater  = LayoutInflater.from(this);

… une méthode pour « inflater » ...
private TableRow inflateTableRow(){
       TableRow tableRow = new TableRow(this);
       return (TableRow) inflater.inflate(R.layout.row_layout,tableRow,false);
}
… et pour charger les vues
TableLayout tableLayout = (TableLayout) findViewById(R.id.tableLayout);    
    // …
for (int i = 0; i<N ; i ++) {    
    // layout inflation            
    TableRow tableRow = inflateTableRow();
                
      // TextView initialization 
    TextView txtFirstName = (TextView) tableRow.findViewById(R.id.txtFirstName);
    txtFirstName.setText("firstName"+ i);

    TextView txtLastName = (TextView) tableRow.findViewById(R.id.txtLastName);
    txtLastName.setText("lastName"+ i);

    tableLayout.addView(tableRow);
}    

Conclusion
Il y aura sans doute d'autres éléments à ajouter dans cet article. La meilleure manière d'exploiter les fonctionnalité de Android est d'utiliser les « ressources » autant que possible. Ca permet d'avoir des programmes robustes – facilement adaptables aux différentes configurations - et évolutifs grâce à la séparation claire entre le code métier et le code statique.