Un client Rest pour Android avec Spring-Android


Spring Android est une extension du framework Spring qui, comme son nom l'indique, vise à simplifier les développements Android. Plus précisément, il s'agit du "portage" sur Android de certains modules du framework. Âgée de seulement quelques mois, Spring-Android contient pour le moment RestTemplate et CommonsLogging.

Dans cet article, je vais présenter RestTemplate dans une application Android minimale qui interroge le service web de Google Maps pour récupérer la liste des adresses correspondant à la requête. RestTemplate, pour faire court, facilite le développement d'un client Rest.

Configuration


Commençons par installer Spring-Android. J'utilise Maven pour builder mon application, je déclare donc mes dépendances dans mon pom.xml. En plus de Spring-Android, je vais également utiliser Jackson.

<dependency>
    <groupId>org.springframework.android</groupId>
    <artifactId>spring-android-rest-template</artifactId>
    <version>1.0.0.M2</version>
</dependency>
<dependency>
	<groupId>org.codehaus.jackson</groupId>
	<artifactId>jackson-mapper-asl</artifactId>
	<version>1.7.1</version>
</dependency>
<dependency>
	<groupId>org.codehaus.jackson</groupId>
	<artifactId>jackson-core-asl</artifactId>
	<version>1.7.1</version>
</dependency>

UI


La partie UI de l'application (constituée d'une seule Activity) est très simple : un champs d'édition, un bouton et une ListView qui nous permettra d'afficher les résultats. Voici le layout

<?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">
    <EditText
    	android:id="@+id/adress" 
    	android:layout_width="fill_parent"
    	android:layout_height="wrap_content"/>
    <Button
    	android:id="@+id/find"
    	android:text="Find"
    	android:layout_width="fill_parent"
    	android:layout_height="wrap_content"/>
    <ListView
    	android:id="@+id/results"
    	android:layout_width="fill_parent"
    	android:layout_height="fill_parent"/>
</LinearLayout>

et la méthode onCreate de l'activity correspondante

@Override
public void onCreate(Bundle savedInstanceState) {
	super.onCreate(savedInstanceState);
	setContentView(R.layout.main);
	find = (Button) findViewById(R.id.find);
	address = (EditText) findViewById(R.id.adress);
	listView = (ListView) findViewById(R.id.results);
	adapter = new ArrayAdapter<Address>(getApplicationContext(), android.R.layout.simple_list_item_1);
	listView.setAdapter(adapter);
	find.setOnClickListener(this);
}

Google Geocode


Le service que je vais utiliser est le service geocode de Google. Il s'interroge de la manière suivante: http://maps.googleapis.com/maps/api/geocode/{format}?address={address}&amp;sensor=false avec format à choisir parmi json et xml et address à remplir par notre requête. Voici donc la manière la plus simple d'interroger ce service.

String address = "18 rue de la monnaie";
String url = "http://maps.googleapis.com/maps/api/geocode/json?address={address}&sensor=false";
RestTemplate restTemplate = new RestTemplate();
String result = restTemplate.getForObject(url, String.class, address);

La méthode getForObject prend en paramètres l'url du service, le type de retour - ici j'ai utilisé le type String pour obtenir la réponse Json brute, mais je vais montrer par la suite qu'on peut faire beaucoup plus acceptable - et les paramètres de l'url. Ici je n'avais qu'un seul paramètre mais on peut en mettre autant qu'on veut.

Jackson


Je vais montrer maintenant comment facilement parser le réponse Json pour obtenir un objet représentant le résultat. Notre requête nous retourne une réponse Json au format décrit dans ce document. On commence par indiquer à notre RestTemplate le convertisseur à utiliser pour les réponses Json. J'utilise ici le convertisseur Json Jackson.

restTemplate = new RestTemplate();

MappingJacksonHttpMessageConverter jsonConverter = new MappingJacksonHttpMessageConverter();

List<MediaType> supportedMediaTypes = new ArrayList<MediaType>();
supportedMediaTypes.add(MediaType.APPLICATION_JSON);
jsonConverter.setSupportedMediaTypes(supportedMediaTypes);

List<HttpMessageConverter<?>> listHttpMessageConverters = restTemplate.getMessageConverters();
listHttpMessageConverters.add(jsonConverter);
restTemplate.setMessageConverters(listHttpMessageConverters);

On écrit ensuite les classes de mapping.

public static class Addresses
{
	public String status;
	public Address[] results;
}
	
@JsonIgnoreProperties({"address_components", "types", "geometry", "partial_match"})
public static class Address
{
	public String formatted_address;
	
	@Override
	public String toString() {
		return formatted_address;
	}
}

Ce que nous renvoie notre service est donc un objet Addresses constitué d'un statut et d'un tableau d'objets de type Address. Il suffit de nommer les propriétés de la même manière qu'elles sont nommées en Json. Pour les adresses, je ne m’intéresse qu'à leur champ formatted_adress, j'utilise donc l’annotation @JsonIgnoreProperties pour lister toutes les propriétés qui ne m’intéressent pas. J'en profite pour redéfinir toString() en vue de l'affichage ultérieur. Il ne me reste plus qu'à demander à mon RestTemplate de m'obtenir un objet Addresses.

Addresses addresses = restTemplate.getForObject(URL, Addresses.class, address);

Requête Asynchrone


Il est temps maintenant d’insérer notre RestTemplate dans notre Activity. J'appelle le RestTemplate au clic sur le bouton. Mais pour ne pas figer l'UI, j'ai utilisé une AsyncTask.

public void onClick(View view) {
	if(view.getId() == R.id.find){
		CharSequence csAddress = address.getText();
		if(!TextUtils.isEmpty(csAddress)){
			new GetRestTask().execute(csAddress);
		}
	}
}
	
private class GetRestTask extends AsyncTask<CharSequence, Void, Addresses>
{
	@Override
	protected Addresses doInBackground(CharSequence... address) {
		if(address != null && address.length > 0)
		{
			try
			{
				return getRestTemplate().getForObject(URL, Addresses.class, address[0]);
			}
			catch (RestClientException e) {
				return null;
			}
		}
		return null;
	}
	@Override
	protected void onPostExecute(Addresses addresses) {
		adapter.clear();
		if(addresses == null || addresses.status.equals(NO_RESULTS))
		{
			Toast.makeText(getApplicationContext(), "Non trouvée ou erreur", Toast.LENGTH_SHORT).show();
		}
		else
		{
			Log.d(TAG, "onPostExecute : "+addresses.status);
			for(Address addr : addresses.results)
			{
				adapter.add(addr);
			}
		}
	}
	private RestTemplate getRestTemplate() {
		if(restTemplate == null)
		{
			restTemplate = new RestTemplate();
			[...]
		}
                return restTemplate;
       }
}

Résultat


Les sources de l'application sont disponibles sur le Github de Zenika

Spring-Android-Device


Commentaires

1. Le jeudi 17 mars 2011, 11:29 par Nicholas

Merci cet exemple, je vais tenter une implémentation correcte !

2. Le jeudi 17 mars 2011, 17:27 par Piwaï

Très bon example d'utiliser de Spring Android :-)

J'ai deux questions/ remarques :

- Tu utilises le Maven Android Plugin.. avec quel IDE ? Eclipse ? En incluant M2Eclipse + m2eclipse-android-integration ? J'ai encore testé il y a quelques semaines, et jme prenais 5 secondes de freeze (rebuild) à chaque sauvegarde d'un fichier de l'appli Android. Comment fais-tu ?

- @JsonIgnoreProperties permet d'éviter que Jackson lache une erreur en indiquant qu'il a trouvé des propriétés non mappées. En pratique, je préfère configurer Jackson pour ne pas produire d'erreur si une propriété n'est pas mappée. En effet, cela a l'avantage de permettre de faire évoluer l'API Rest (ajout de champs) sans que les clients en soient immédiatement affectés.

3. Le lundi 21 mars 2011, 20:32 par Guillaume Gerbaud

Merci pour ce tuyau Jackson ;-)

J'utilise Eclipse avec effectivement M2eclipse + m2eclipse-android-integration.

J'utilise également l'archetype suivant https://github.com/akquinet/android-archetypes  à la création d'un projet.

Je n'ai aucun problème de freeze.

Par contre sur un projet que j'ai "Mavenisé", après une sauvegarde, il arrive que le build Maven se lance plus d'une fois.

4. Le dimanche 17 avril 2011, 00:35 par Pascal Libiero

On a depuis la même dans le monde .NET avec Spring.NET REST qui supporte Silverlight et Windows Phone :
http://springframework.net/index.ht...

5. Le dimanche 4 mars 2012, 00:18 par galex

Merci pour cet article, très intéressant!

6. Le mardi 22 mai 2012, 00:04 par Stéphane Nicolas

Un très bon article. Cependant, une implémentation plus robuste d'un service de communication pourrait nécessiter une mise en oeuvre plus difficile : les AsyncTask ont un cycle de vie qui est contenu dans celui de l'activité. Si la requête prend un peu de temps, les utilisateur/TRICEs vont vouloir quitter l'activité pour y revenir. Or cela tuera l'AsyncTask. Il vaut alors mieux privilégier un Service pour effectuer la requête, celle-ci pourra s'exécuter quoiqu'il arrive à l'activité qui a initié l'appel REST.

Une telle solution n'est cependant pas sans problème non plus puisqu'elle va demander la mise en oeuvre d'un processus de communication activité-service qui possède de sérieuses limitations (requiert une forme de sérialization des objets passés en paramètre).

http://stackoverflow.com/questions/...

L'exemple était très bien fait. Merci.

Fil des commentaires de ce billet

Ajouter un commentaire

Le code HTML est affiché comme du texte et les adresses web sont automatiquement transformées.