Organiser les accès aux données sous Android

En réalisant une application « Cave à vins » sous Android, je me suis trouvé face à un problème de taille: comment faire passer mes objets métiers d’une Activity à l’autre en gardant un code maintenable?

Tout dans les Intents!

Ma toute première approche a été de tout envoyer dans les Intents, à chaque changement de vue, j’ai envoyé des ArrayLists d’objets métier entiers. Je me suis vite rendu compte que ça devenait rapidement ingérable. Il faut traiter trop de cas différents dans chaque page: que se passe-t-il avec mes données si l’utilisateur clique sur « back »?
Intent i = new Intent(DisplayDetail.this, MarkedMap.class);
ArrayList<Marker> mm = new ArrayList<Marker>();
mm.add(new Marker(1, 2, 3));
i.putParcelableArrayListExtra("markers", mm);
startActivityForResult(i, CODE_REQ_MARKER);

Avec un ContentProvider

Ma seconde approche fut d’utiliser un Content Provider, puisque son nom dit qu’il fournit des données, ça devrait être ce que je veux. Ca a l’air super, y’a une méthode « managedQuery » qu’on peut appeler de n’importe quelle Activity. Le souci, c’est qu’il fournit des objets de type Cursor. Et là, si on a besoin dans deux Activity de la liste des Personnes, on doit écrire deux fois la boucle sur le Cursor:
Cursor cur = managedQuery(myPerson, null, null, null, null);
if (cur.moveToFirst()) {

        String name;
        String phoneNumber;
        int nameColumn = cur.getColumnIndex(People.NAME);
        int phoneColumn = cur.getColumnIndex(People.NUMBER);
        String imagePath;

        do {
            // Get the field values
            name = cur.getString(nameColumn);
            phoneNumber = cur.getString(phoneColumn);

            // Do something with the values.
            ... 

        } while (cur.moveToNext());

    }

(exemple pris sur developer.android.com)

Et ça, c’est terrible. Si un jour on change le modèle métier des Personnes, faut réécrire ce code dans CHAQUE Activity.

L’héritage, tout simplement

Vous me direz, mais pourquoi ne pas avoir une superclass qui s’occupe de tout ça et qui renvoie des objets métiers comme on veut? Ca semble être une super idée, on a une classe qui contient un ensemble de méthodes pour récupérer, lister, modifier nos objets Personne. Et puis toutes les Activity qu’on crée qui ont besoin de ces Personnes héritent de notre superclasse. Si on veut faire les choses bien, on peut même envisager que la super classe est abstraite et qu’on a une flopée de classes du style « DAOimpl » qui vont aller chercher les données dans un flux xml, la base de donnée interne SQLite ou encore un webservice ou que sais-je?
C’est une bonne idée, à un détail près: sous Android, quand on passe d’une Activity à l’autre, ce n’est pas le développeur qui appelle explicitement le constructeur de l’Activity. C’est géré de façon interne (et obscure à mes yeux!), avec un classloader. Le développeur ne fait qu’appeler « startActivity(Intent i…) » ou encore « startActivityForResult(Intent i….) » et c’est à peu près tout ce qu’on peut contrôler sur l’instanciation de l’Activity.
Et donc si dans la superclasse on gère une collection de Personne, on risque fortement (pour ne pas dire inévitablement) d’avoir plusieurs collections de Personne qui se baladent en mémoire de façon parallèle! Et donc éventuellement une des instances de la collection qui sera modifiée par une Activity, une autre instance de la collection de Personne par une deuxième Activity…
Enfer et damnation, ce n’est donc toujours pas la bonne solution.
La solution, c’est d’utiliser un service. Le service Android, c’est comme un thread qui tourne en parallèle de votre application, qui peut soit être lancé et s’arrêter en même temps que chaque Activity, soit tourner indéfiniment, jusqu’à ce qu’on lui dise explicitement de s’arrêter.
Donc, l’idée, c’est que chaque vue qui a besoin des Personnes se connecte au service:
Intent dbServiceIntent = new Intent("com.oxiane.service.DBService");
ServiceConnection conn = new ServiceConnection() {
			@Override
			public void onServiceConnected(ComponentName name,IBinder service) {
				Log.d("dbService", callingClassName + " Connected");
				dbService = ((DBService.MyServiceBinder) service).getService();
				serviceConnectionIsReady();
			}

			@Override
			public void onServiceDisconnected(ComponentName name) {
				Log.d("dbService", callingClassName + " Disconnected");
			}
		};
bindService(dbServiceIntent, conn, Context.BIND_AUTO_CREATE);

Note: la méthode serviceConnectionIsReady() est une méthode que j’ai mise en abstraite dans la superclass de mes Activity. Chaque Activity va donc devoir l’implémenter. Un bon exemple d’utilisation est, par exemple, d’afficher une barre de progression « Loading… » puis, dans la méthode serviceConnectionIsReady(), vous avec accès à vos données, donc vous pouvez afficher votre vue avec les données.

Il reste encore un souci: dans ce cas d’utilisation, chaque Activity qui appelle ce bout de code va lancer le service et l’arrêter quand l’Activity meurt. Ce qu’on veut, nous, c’est que le service soit lancé tout le temps, et que chaque Activity puisse à sa guise, se connecter ou non au service.

On pourrait faire simplement un « startService » dans un coin, sur la première Activity de l’application. Mais ce serait trop simple, non? Et on risque d’oublier que c’est dans telle ou telle Activity que le service est démarré. Et il faudrait changer de place ce code si on décide que c’est finalement une autre Activity qui se lance en premier…

Au lieu de ça, il suffit de vérifier si notre service « com.oxiane.service.DBService » est lancé ou non:

Lister les services qui tournent sur votre machine

Il y’a justement un service Android qui est fait pour ça! Le service ACTIVITY_SERVICE:

final ActivityManager activityManager = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
final List<RunningServiceInfo> services = activityManager.getRunningServices(Integer.MAX_VALUE);

Et voilà, plus qu’à parcourir cette liste, et on peut lancer le service s’il n’existe pas, ou s’y connecter s’il existe!

for (int i = 0; i < services.size(); i++) {
if ("com.oxiane.service.DBService".equals(services.get(i).service.getClassName())) {
isServiceFound = true;
break;
}
}
if(isServiceFound)
{
Log.d("oxianeService", "Service trouvé, je me connecte!");
bindService(dbServiceIntent, conn, Context.BIND_AUTO_CREATE);
}
else
{
Log.d("oxianeService", "Service NON trouvé, je le crée puis je me connecte");
startService(dbServiceIntent);
}