Blog

Appel à des services Web sous Android – Feuilleton de l’été 3/4

androx03Comme annoncé lors de l’épisode précédent, nous allons compléter notre code afin de récupérer le fichier correspondant au catalogue Oxiane et de le présenter à l’utilisateur.

Dans cette étape, nous construirons une liste de filières afin de permettre à l’utilisateur de choisir dans le catalogue de formations la filière qui l’intéresse. Cette étape permettra d’aborder :

  • Comment appeler un service web avec Android et traiter le document XML retourné pour en extraire les éléments significatifs. Nous montrerons comment choisir un modèle de traitement XML sous Android.
  • Comment positionner une liste sur un écran et comment attacher des actions aux items de la liste afin de prendre en charge la sélection de l’utilisateur.

Ah, si dans la vie tout se passait bien d’un coup comme on l’aurait souhaité, ce serait merveilleux, non ? Hélas il arrive très souvent que des imprévus s’y mêlent aussi. En programmation ce sont généralement des exceptions. Nous allons aussi voir comment déboguer une application Android.
Dans notre classe CatalogueOxiane nous récupérons la liste des filières de formation par un service web REST-RPC (pour reprendre la dénomination trouvée par les auteurs de RESTFul Web services). Pour ce faire, nous allons créer un objet Catalogue. Cet objet va contenir la liste des filières de formation. Nous utiliserons aussi une base de données SQLite pour stocker les filières de formation.

Pour commencer, modifiez la définition de la classe CatalogueOxiane pour qu’elle hérite de la classe ListActivitity. (public class CatalogueOxiane extends ListActivity). Le compilateur vous propose d’importer la classe ListActivity. ListActivity est une sous-classe de Activity qui offre des fonctionnalités supplémentaires permettant de travailler sur les listes.

La classe Catalogue

Créez la classe Catalogue comme indiqué sur le listing 4. Cette classe comprend un membre (mfilieres) qui est une liste d’objets Filiere et les méthodes addFiliere (qui ajoute une filière à la liste), getFilieres (qui retourne la liste des filières) et getFiliere (qui retourne la filière qui se trouve à une position donnée de la liste).

public class Catalogue {
	
	private ArrayList<Filiere> mfilieres = new ArrayList<Filiere>();
	
	public void addFiliere (Filiere filiere){
		this.mfilieres.add(filiere);
	}
	
	public ArrayList<Filiere> getFilieres(){
		return this.mfilieres;
	}
	
	public Filiere getFiliere(int i){
		return (Filiere) this.mfilieres.get(i);
	}

}

Listing 4. La classe Catalogue

Bien sûr le compilateur se plaint parce que nous n’avons pas encore créé la classe Filiere. Avant de créer cette classe, jetons un coup d’œil à la structure du fichier attendu du service web (fig.1.). Nous constatons qu’un élément filiere comprend 2 attributs qui le définissent : fichier et titre.

<filieres>
  <filiere fichier=’filiere_methodes’ titre=’Modelisation et Méthodes’/>
  <filiere fichier=’filiere_soa’ titre=’Architecture Orientée Services’/>
</filieres>

Fig.1 . La liste des filières

Sur la base des éléments fournis par ce fichier, nous créons la classe Filiere comme indiqué au listing 5. Cette classe comprend 2 membres (mfichier et mtitre) et leurs accesseurs respectifs getFichier, getTitre, setFichier et setTitre. Sous Android, il est de convention que les variables membres de classe soient préfixées par la lettre ‘m’ qui signifie member.
Notons que les classes Catalogue et Filiere ne présentent aucune spécificité liée à l’API Android.

package com.oxiane.catalogueoxiane;
public class Filiere {
	String mfichier, mtitre;
	public String getFichier() {
		return mfichier;
	}
	public void setFichier(String mfichier) {
		this.mfichier = mfichier;
	}
	public String getTitre() {
		return mtitre;
	}

	public void setTitre(String mtitre) {
		this.mtitre= mtitre;
	}
}

Listing 5. La classe Filière

Traitement XML sous Android

La liste des filières nous est transmise au format XML (voir fig.1). Nous devons alors parser ce fichier afin d’y extraire les éléments qui vont constituer notre objet Catalogue. Il existe deux principales API (Application Programming Interface) pour traiter les documents XML : DOM (Document Object Model) et SAX (Simple API for XML). L’API DOM construit en mémoire l’arbre du document avant le traitement alors que l’API SAX traite le document XML de façon événementiel. Etant donné que le document à traiter peut être volumineux et vu les contraintes physiques limitées des téléphones mobiles, charger l’arbre du document en mémoire peut s’avérer être une solution coûteuse. Nous allons utiliser l’API SAX.

Créez la classe SaxCatalogueHandler qui va être chargée de parser le fichier XML des filières et nous retourner un objet Catalogue via un accesseur. Nous fournissons cette classe en listing 6. Elle hérite de la classe DefaultHandler, qui fournit les implémentations par défaut de tous les callbacks de SAX2 qui vont servir à réagir aux « événements » survenant à la lecture du fichier xml : ouverture d’un tag, lecture d’un élément se trouvant entre deux tags, lecture d’un attribut d’un tag, fermeture d’un tag etc.
La variable mCurrentAttList de type Attributes est destinée à contenir la liste des attributs de l’élément courant. L’accesseur getCatalogue() permet de récupérer l’objet Catalogue constitué lors du parsing. Le Catalogue est construit dans la méthode startElement() qui est exécutée lorsque le parseur rencontre une balise ouvrante. Nous constituons alors un objet Filiere que nous ajoutons au Catalogue. Le but de ce tutoriel n’étant pas de passer en revue l’API SAX, nous n’allons pas élaboré longuement sur cette classe. Notons pour résumer que suivant l’API SAX, le développeur doit mettre en place un modèle qui prend en charge les différents événements (startDocument(), endDocument(), startElement(), …). C’est le rôle que joue la classe SaxCatalogueHandler. Aussi l’objet (ou les objets) construits dans cette classe Handler doit avoir un accesseur afin d’être accessible après la phase de parsing.


package com.oxiane.catalogueoxiane;

import org.xml.sax.*;   // Pour faire court et pour avoir un listing pas trop important
import org.xml.sax.helpers.DefaultHandler;

public class SaxCatalogueHandler extends DefaultHandler{ 
// les membres de la classe préfixés par m suivant les conventions //Android
   private Catalogue mCatalogue = new Catalogue();
   private Filiere mFiliere = null; 
   //liste des attributs de l’élément courant 
   private Attributes mCurrentAttList = null;
   private static String TAG_FILIERE = "FILIERE";

   //l’accesseur pour récupérer l’objet Catalogue
   public Catalogue getCatalogue(){    
	   return this.mCatalogue; 
	}
   @Override
   public void startDocument() throws SAXException {
       this.mCatalogue = new Catalogue();
  }
   @Override
   public void endDocument() throws SAXException {
        // Rien à faire
   }
   @Override
   public void startElement(String namespaceURI, String localName,
             String qName, Attributes atts) throws SAXException {
        if (localName.equalsIgnoreCase(TAG_FILIERE)) {
        	//creer un objet filiere
		this.mFiliere = new Filiere();
			
		this.mCurrentAttList = atts;			
		for (int i =0; i<mCurrentAttList.getLength(); i++){
			String aName = mCurrentAttList.getLocalName(i);
			String value = mCurrentAttList.getValue(i);
				if(aName.equalsIgnoreCase("fichier")){
					this.mFiliere.setFichier(value);
				}else if(aName.equalsIgnoreCase("titre")){
					this.mFiliere.setTitre(value);
				}				
			}//end for
		//inserer l'objet filiere créé dans le catalogue
		//et initialiser mFiliere
			this.mCatalogue.addFiliere(mFiliere);
			this.mFiliere = null;
        }        
   }
   
   @Override
   public void endElement(String namespaceURI, String localName, String qName) throws SAXException {
        //Rien à faire 
   } 	
}

Listing 6 : SaxCatalogueHandler

Maintenant que nous avons récupéré les donénes contenues dans le fichier XML des filières, nous pouvons appeler le service web, récupérer le document retourné, et constituer notre objet Catalogue.
Dans la classe CatalogueOxiane, créer la méthode callService comme indiquée par le listing 7. Le compilateur ne reconnaît pas la variable serviceLocation. Cette variable est destinée à contenir l’url de notre service XML-RPC. Il s’agit en fait d’une constante que nous vous invitons à créer comme suit :

private static final String serviceLocation = ‘http://oxiane.com/rest/GetFilieres’;

La méthode callService commence par instancier un objet URL, puis obtient une interface de lecture de document XML par callBacks (XMLReader SAX). La suite consiste à :

  • Indiquer au reader notre gestionnaire des événements (la méthode setContentHandler ()).
  • Ouvrir la connexion à l’url et obtenir le stream XML (la méthode url.openStream()).
  • Parser la source reçu (la méthode parse()).
  • Finalement, via l’accesseur getCatalogue() cette méthode retourne l’objet Catalogue.
    protected Catalogue callService(){		
		Catalogue catalogue = null;
		try {
            /* URL à partir duquel nous récuperons le fichier XML */
            URL url = new URL(serviceLocation);

            /* Obtenir le parseur SAXParser depuis SAXPArserFactory. */
            SAXParserFactory spf = SAXParserFactory.newInstance();
            SAXParser sp = spf.newSAXParser();

            /* Obtenir le XMLReader du SAXParser que nous venons de créer */
            XMLReader xr = sp.getXMLReader();
            /* Créer un nouveau ContentHandler et l’appliquer au XML-Reader*/
            SaxCatalogueHandler myHandler = new SaxCatalogueHandler();
            xr.setContentHandler(myHandler);
            
            /* Parser le document XML retourner par l’URL. */
            xr.parse(new InputSource(url.openStream()));
            /* Le parsing est terminé */

           /* Récupérons l’objet Catalogue via l’accesseur de notre Handler */
            catalogue = myHandler.getCatalogue();            
            
       } catch (Throwable e) {
    	   e.printStackTrace();            
       } 
		
		return catalogue;
	}

Listing 7. callService

Le catalogue ainsi obtenu depuis le service web, préparons l’interface pour son rendu à l’utilisateur.

L’interface graphique de l’activité

Nous procédons à présent à l’écriture du fichier décrivant l’interface graphique (layout) et qui va assurer le rendu de notre Catalogue. Créez sous le dossier /res/layout le fichier catalogue.xml tel qu’indiqué à la figure 2.
Ce fichier définit un conteneur linéaire (LinearLayout) pour accueillir une liste d’items. Le symbole @ dans la valeur de l’attribut id de l’élément ListView signifie que le parseur XML doit parser et rechercher dans la chaîne une ressource id. Puisque la ressource id nommée list est fournit par le système Android, nous devons préfixer le mot id par la chaîne « android :» (@android :id/list).
Ce fichier de layout a littéralement la signification suivant : ceci est un fichier d’interface android constitué d’un conteneur linéaire (l’élément LinearLayout, qui est un conteneur qui arrange ses enfants dans une seule ligne (de manière horizontale) ou dans une seule colonne (de manière verticale)). Ce conteneur doit occuper la hauteur (l’attribut layout_height) et la largeur (l’attribut layout_width) de l’écran nécessaire pour le rendu de son contenu (la valeur wrap_content) et sa couleur de fond est déterminée par le code de couleur #b3d3e4. Dans ce conteneur, on n’affiche qu’une chose, la liste d’éléments (élément ListView).

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout    
      xmlns:android="http://schemas.android.com/apk/res/android"
      android:layout_width="wrap_content"
      android:background="#b3d3e4"
      android:layout_height="wrap_content">

   <ListView android:id="@android:id/list"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/> 
</LinearLayout>

Fig.2. catalogue.xml

La figure 2 montre que le conteneur contient une liste d’éléments, mais alors quel type d’éléments ? Nous devons définir une vue qui s’appliquera pour chaque ligne de notre ListView. Créer sous le dossier /res/layout le fichier branch_row.xml comme indiqué à la figure 3.

<LinearLayout    
      xmlns:android="http://schemas.android.com/apk/res/android"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content">

   <TextView android:id="@+id/branch"
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:textStyle="bold"
    android:textColor="#000000"
    android:layout_height="wrap_content"/>

Fig.2. branch_row.xml

Il s’agit ici de la vue qui sera utilisée pour afficher le titre de chaque filière.
Dans le cas présent nous avons créé un TextView avec un id appelé branch. C’est par cet id que nous accéderons à cette vue depuis notre code source. Le signe + après le symbole @ indique que l’id sera automatiquement créé comme une ressource si elle n’existe pas déjà.
Retournons dans la classe CatalogueOxiane. Modifions la méthode onCreate () pour avoir le listing 9.
Le compilateur ne reconnaît pas la méthode fillData(), normal ! Elle n’a pas encore été créée. C’est cette méthode qui va parcourir le catalogue et l’afficher.

public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.catalogue);	
		fillData() ;		
	}

Listing 9. onCreate de l’activity

ListAdapter et ArrayAdapter

Pour faciliter le traitement des listes, les concepteurs d’Android ont défini une notion d’« adaptateur » qui sert de pont entre d’un côté un ListView qui porte une série d’éléments à afficher – et l’autre côté l’ensemble de données qui alimente la ListView.
Afin de traiter de l’adaptation d’une List d’objets, il est fourni une spécialisation de cette interface nommée ListAdapter. Cette interface sert de base à l’implémentation de plusieurs sous-classes parmi lesquelles ArrayAdapter, qui sert à mapper un tableau au composant graphique ListView. Bien, vous avez compris, nous allons instancier un ArrayAdapter avec la liste des titres de filières de notre catalogue et indiquer ce dernier à Android comme notre source de données et le tour sera joué.

Créer la méthode fillData() comme indiqué au listing 10.

private void fillData() {

  //appel à la méthode callService() pour récupérer le catalogue
  Catalogue oxCatalogue = callService();

  //instancions un ArrayList pour accueillir 
  //les titres de filières à afficher
  ArrayList<String> listeTitres = new ArrayList<String>();
  for (Filiere f: oxCatalogue.getFilieres()) {	    
    listeTitres.add(f.getTitre());
  }

  // ArrayAdapter pour afficher la liste des filières
  ArrayAdapter<String> branchAdapter = 
       new ArrayAdapter<String>(this, R.layout.branch_row,    
                                          R.id.branch, listeTitres);
  //Indiquer l'adapter au système pour l'affichage
  setListAdapter(branchAdapter);
}

Listing 10. fillData()

Nous utilisons le constructeur suivant pour ArrayAdapter : ArrayAdapter (Context context, int resource, int textViewResourceId, T[] objects), où :

  • Le paramètre context désigne le contexte courant
  • resource est le fichier de layout des lignes à afficher (branch_row.xml)
  • textViewResourceId est l’id de l’élément TextView dans la ressource layout
  • Et finalement objects contient les objets à représenter dans la ListView.

La méthode ListActivity.setListAdapter() permet d’indiquer à Android l’Adapter à utiliser pour notre ListView. Elle permet aussi au système de gérer un curseur sur la ListView. Ainsi quand l’utilisateur clique sur un item de liste, nous pouvons récupérer depuis le code la position cliquée. Nous élaborerons cet aspect ultérieurement quand il faudra prendre en compte le choix de l’utilisateur.
A présent exécutons l’application (Ctrl+F11). Il s’affiche à l’écran un message d’erreur : « The Application Catalogue Oxiane has stopped unexpectedly. Please try again ». Le système nous invite à réessayer. Non, nous n’allons pas le faire. Quittez cette fenêtre ! Déboguons notre application.

Déboguer une application Android

Mettez un point d’arrêt au tout début de la méthode callService() qui se trouve dans la classe CatalogueOxiane. Lancez l’application en mode débogage (menu contextuel Run > Debug As > Android Application). Naviguez dans cette classe, vous constatez qu’une exception est levée lors de l’appel à la méthode parse () (xr.parse(inew InputSource(url.openStream)) ;). L’exception signalée est : java.net.SocketException: Permission denied (maybe missing INTERNET permission).

Cette exception est due au fait que la méthode parse () requiert la connexion à l’url indiquée afin de récupérer le document à parser, mais il se trouve que nous n’avons pas donné les permissions nécessaires à l’application. En effet sous Android lorsqu’une application implémente une action susceptible d’avoir une influence sur le système ou sur les autres applications (telle l’accès à Internet, l’interception d’appel, etc…), elle doit solliciter au préalable la permission adéquate. Ceci permet au système, lors de l’installation , d’avertir l’utilisateur des risques liés à ces permissions.
Ouvrez le fichier AndroidManifest.xml. Suivez l’onglet permissions, cliquez sur add pour ajouter une permission, puis dans la fenêtre qui s’affiche, cliquez sur Uses permissions. Dans la liste déroulante proposée, choisissez «android.permission.INTERNET».
Le noeud suivant est alors ajouté à la fin de votre fichier AndroidManifest.xml.

Relancez l’application ! Vous obtenez alors l’écran suivant qui présente la liste des filières de formation disponible chez Oxiane.

capt2

Nous observons une curiosité sur cet écran. Il est divisé en deux parties : une partie supérieure avec un fond bleue ciel et une partie inférieure à fond noir. Ceci se justifie par le fait que dans le fichier de layout catalogue.xml, la hauteur du layout englobant la liste a été défini à la valeur ‘wrap content’, qui signifie que la liste occupe la partie de l’écran nécessaire pour son affichage (android:layout_height= »wrap_content »). Faites passer cet attribut à ‘fill_parent’ (android:layout_height= »wrap_content ») et observez l’effet de cette modification à l’écran. En définissant la hauteur de l’écran à fill_parent, nous voulons que ce dernier occupe tout l’espace alloué par le conteneur parent. Après cette modification vous obtenez l’écran suivant:

capt3

Cliquez sur un élément de la liste. Il ne se passe rien.   Nous allons nous à écouter les événements survenant sur notre liste – au travers d’un Listener.
Notre objectif final est d’afficher à l’utilisateur la liste des cours lorsque l’utilisateur sélectionne une filière sur l’interface graphique. Mais pour le moment nous allons juste faire passer à l’écran un message court indiquant la position à laquelle l’utilisateur à cliquer. Le but est d’illustrer aussi simplement que possible la gestion des événements utilisateur. Par la même occasion nous illustrons le concept très élégant de notification de messages via les Toasts.
Nous allons surcharger le callback onListItemClick(). Il est appelé lorsque l’utilisateur sélectionne un item de la liste. Il accepte quatre paramètres : l’objet ListView depuis lequel il est appelé, la vue dans l’objet ListView où l’utilisateur à cliquer, la position cliquée dans la liste, et finalement l’identifiant mRowId de l’item sélectionné. Dans notre cas, nous nous intéressons essentiellement à la position sélectionnée.
Notre implémentation de la méthode onListItemClick () est fournie en listing 11. La méthode Toast.makeText ().show() permet d’afficher un message à l’écran sans interrompre l’utilisateur.

protected void onListItemClick(ListView l, View v, int position,  
                                                       long id) 
{
  super.onListItemClick(l, v, position, id);  

  // Afficher un Toast à l’écran avec la position cliquée 
  Toast.makeText(this, "vous êtes à la position: "+position, 
               Toast.LENGTH_SHORT).show();
                                         
}

Listing 11. onListItemClick

Relancez l’application  et sélectionnez par exemple la filière « Java et Java EE ». Vous obtenez cet écran:

capt4

Le message à l’écran vous indique la position de la filière choisie. Vous pouvez vérifiez en comptant à partir de zero. Tout est ok ? Bien, ceci termine cette étape de notre voyage. En fait le fond bleu me rappelle que c’est l’été et que l’eau est … ah ah .

Récapitulons

Nous avons construit tout au long de ce trajet une activité qui permet d’interroger un service REST-RPC, de parser le document XML retourné et de l’afficher. Nous avons aussi écrit un gestionnaire d’événements qui répond aux actions de l’utilisateur par un toast indiquant la position sélectionnée.
Lors de notre prochaine étape, nous nous servirons de la position choisie par l’utilisateur pour récupérer l’identifiant de la filière. A partir de cet identifiant irons récupérer toujours via un appel à un service REST-RPC la liste des cours de la filière sélectionnée.
Il nous semble important de relever que l’appel à la méthode url.openStream() tel que effectué jusqu’à présent est bloquant. De plus toute interruption de connexion résultera à un message très peu élégant pour l’utilisateur. Nous introduirons les threads sous Android dans la suite.
A chaque fois que l’utilisateur démarre l’application, une requête est effectuée vers le service web pour récupérer la liste des filières. Ceci peut entraîner une surcharge injustifiée du réseau, hormis le risque d’indisponibilité des données qui peut se poser. Dans la suite nous introduirons le moteur de base de données embarquée SQLite. Ainsi lors du premier démarrage le catalogue sera téléchargé et stocké dans une base de données. Ensuite l’utilisateur pourra utilise la base locale. Vous imaginez tous les bénéfices d’une telle approche.
A très bientôt donc !

Auteur : Majirus Fansi


Retrouvez l’ensemble des épisodes de notre tutoriel Android, le feuiileton de l’été:


Voir également

admin

Written by

The author didnt add any Information to his profile yet

  • Pingback: Android : Hello world ! - Feuilleton de l’été 2009 épisode 2 | Sur la route d'Oxiane()

  • Pingback: Introduction à Android - Feuilleton de l’été 2009 épisode 1 | Sur la route d'Oxiane()

  • Pingback: Android : Threading, Moteur de base de données SQLite - Feuilleton de l’été épisode 4 | Sur la route d'Oxiane()

  • javaiste

    ça serait bien d’avoir le code source en entier car là petit bout par petit bout on s’y perd. Surtout pour ce qu’il veule débuter… Juste avant il y avait un bout de code pour un Hello world et ensuite ce tuto très compliqué par rapport au premier. La progression n’est pas vraiment douce et didactique. Pourriez mettre quelques tutos transitoires genre après un hello world montrez comment un bouton ouvre une autre fenetre ou vue?

  • Carles

    Bonour, tout d’abord felicitation pour ces tutoriels si interessants ;) un vrais plaisir.
    D’un autre côté, je suis entreint de les suivre, pourtant je pense avoir tout suivi, mais j’ai des erreurs de ce style (Logcat):
    10-14 23:48:00.051: ERROR/AndroidRuntime(2012): Uncaught handler: thread main exiting due to uncaught exception
    10-14 23:48:00.451: ERROR/AndroidRuntime(2012): java.lang.RuntimeException: Unable to start activity ComponentInfo{com.oxiane.catalogueoxiane/com.oxiane.catalogueoxiane.catalogueoxiane}: java.lang.RuntimeException: Your content must have a ListView whose id attribute is ‘android.R.id.list’

    En suivant le conseil de javaiste, ce serait correct d’avoir le code source pour verifier votre tutoriel.

    Merci d’avance

  • Carles

    En fait ça marche (ma faute), et tres bien, felicitation pour ce tutoriel ;) tout simplement parfait ;)

  • De la part de Majirus : un grand merci :)

  • Pingback: Quelques liens pour le developpement sur Android()

  • ouzlaye

    Bonjour merci pour ce tuto. Mais j’ai un problème depuis un bon moment j’ai utilisé un webview pour créer une app qui ouvre mon site web depuis un terminal android mais le problème est que le webview refuse de lire les videos youtube integrées sur le site. Je voudrais savoir s’il y a moyen de passer par un web service pour contourner le problème