Blog

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);
}
admin

Written by

The author didnt add any Information to his profile yet

  • aboudard

    Bien l’idée de l’accès au service, c’est comme ça que je concevais cet outil justement. A l’usage il faudra tester les performances !

  • Walid

    Et qui est l’auteur de cet article ? Johann H. je présume ?

  • Majirus Fansi

    Well joli article.

    Je trouve le fait de mettre un service au dessus du content provider très clair. Cependant, je pense que la dernière partie (lister les services démarrés et faire la recherche dans la liste) augmente la complexité de l’algorithme.

    Est ce qu’il ne serait pas plus simple de lancer directement le service sans prendre toutes ces précautions. en effet la commande startService(dbServiceIntent)retourne l’objet ComponentName du service s’il est démarré, sinon il retourne Null. Dans tous les cas l’instruction ne démarre pas plus d’une fois le même service et le résultat retourné permet de savoir si oui ou non le service était déjà démarré.

    Une question pour terminer, quand est ce que le service démarré est arrêté? Je ne vois pas cette phase dans le texte.

    Merci encore pour l’approche de l’article.

    Cheers,

    Maj

  • Steph

    Super article !
    On comprend bien le role des differents composants Android : Intent, Activity, Service et ContentProvider

    Merci Johann !!

  • Johann

    Majirus:
    En fait si j’ai pris ces précautions, c’est parce que ça ne marchait pas comme je voulais sans les prendre!
    En fait, pour une raison encore un peu obscure à mes yeux, quand je faisais des startService sur chaque activity, j’arrivais à avoir deux services « dbservice » qui tournent en parallèle. Tu peux en fait avoir un service qui tourne au niveau de l’OS et un qui se lance et s’arrête pour chaque activity. Comme je voulais avoir un seul et unique service pour stocker mes données sans avoir deux variables qui se mettent plus ou moins à jour dans un thread séparé, c’est la seule solution que j’aie trouvé.

    Evidemment, c’est uniquement le cas parce que mes données sont en mémoire. Si mon service faisait un appel à une base de données, on pourrait très bien imaginer qu’il se lance au onCreate de chaque activity et qu’il se stoppe au onStop() ou onDestroy(). Dans ce cas, le service ne serait pas un « data source ».

    Sinon, pour répondre à ta question, hé bien en fait j’y ai déjà répondu. Effectivement, je ne le mentionne pas dans l’article, mais le service est stoppé lorsque l’application se ferme (onStop).

  • Alors il faudrait tirer au clair ce comportement des services j’imagine :)