Bonjour,
J'ai récemment créé un sujet pour une problématique similaire, mais étant donné que j'ai effectué plusieurs changements entre temps et que certains points n'étaient pas clair, j'ai préféré partir sur un nouveau sujet avec une base solide d'explications et de détails. Du coup, le message est un peu long =D
Côté application/fonctionnement
Pour décrire le fonctionnement (simplifié et avec seulement la partie qui nous intéresse ici) de l'application, disons que j'ai :
- Une activité de "démarrage" qui vérifie la connexion internet, certains params, etc.
Si tout est ok, l'activité se termine et lance l'activité Menu.
Si la base de donnée est vierge (premier lancement/données applications supprimées), une AsyncTask est lancée pour récupérer la liste des informations à insérer dans la base de données SQLite (pendant ce temps, l'utilisateur est "bloqué" avec un progressDialog). L'activité Menu est ensuite lancé.
C'est aussi cette activité qui lancera le services de vérification et de mise à jour.
- Une activité qui sert de Menu qui permet à l'utilisateur de choisir la liste à afficher.
Il y a parfois quelques insertion sql à faire ici, l'utilisateur est bloqué par un progressDialog en attendant.
Chaque bouton lancera l'activité "Liste", avec un contenu qui change un peu selon le bouton.
- Une activité Liste pour afficher, comme son nom l'indique, une liste d'infos issue de la base SQLite.
Juste au dessus de la liste, il y a un EditText : il permet de filtrer la liste (ce qui signifie donc l'exécution de requêtes SQL à chaque caractères saisis).
Pour afficher les informations dans la liste, j'ai une classe qui étends CursorAdapter pour extraire les informations et filtrer selon l'EditText. A chaque fois qu'un caractère est saisi, une méthode "filtrer" de l'adapter est appelé et lancera une AsyncTask "FilterTask" (en prenant soin d'annuler le FilterTask précédent s'il y en avait) pour effectuer les requêtes.
Selon les listes, il peut y avoir une mini-requête (une seule ligne) d'ajout ou de suppression, effectuée dans une AsyncTask spéciale lorsque l'utilisateur aura "touché" un élément de liste.
Je pense que je vais me faire taper dessus car je n'utilise pas les Loaders / ContentProvider... mais j'ai assez de mal à comprendre comment ils fonctionnent. Donc j'ai essayé de bricoler un système moi même pour que les traitements SQL et les filtres ne tournent pas dans l'UI. Si vous avez des conseils ou des remarques, je suis preneur.
Côté base de données
J'ai donc ma classe qui étend SQLiteOpenHelper pour construire la table et pour garder une référence statique sur son instance (singleton). J'ai pu lire que c'était essentiel pour faire du multithread.
En ce qui concerne la classe qui contient les méthodes pour effectuer les requêtes SQL, je l'ai modifié pour que l'ouverture et la fermeture de la base fonctionne avec du multithread (je ne sais pas si c'est le meilleur moyen, mais ça a l'air de fonctionner).Code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24 public class DatabaseHelper extends SQLiteOpenHelper { private static DatabaseHelper mHelperInstance = null; public static synchronized DatabaseHelper getHelperInstance(Context context) { if (mHelperInstance == null) { mHelperInstance = new DatabaseHelper(context.getApplicationContext()); } return mHelperInstance; } public DatabaseHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); } }
Code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44 public class ClientsSQLite { private SQLiteDatabase sqliteDatabase; private DatabaseHelper databaseHelper; private static int nb_instance = 0; public ClientsSQLite(Context context) { databaseHelper = DatabaseHelper.getHelperInstance(context); this.sqliteDatabase = databaseHelper.getWritableDatabase(); } public void open() { if (nb_instance == 0) { this.sqliteDatabase = this.databaseHelper.getWritableDatabase(); } nb_instance++; } public void close() { nb_instance--; if (nb_instance == 0) { this.sqliteDatabase.close(); } } public void doMachinTruc(String machin, string truc) { this.sqliteDatabase.execSQL("........."); } ... }
J'ai parfois vu certains bouts de codes qui ne gardaient pas de référence sur SQLiteDatabase, mais seulement sur le DatabaseHelper. Du coup, il n'y avait pas de méthode open/close et à chaque requête, il faut utiliser la méthode getWritableDatabase() pour récupérer une référence.
Q1 : Qu'en pensez-vous ? Ça n'affecte pas trop les performances vu qu'il faut à chaque fois récupérer une référence + allouer un objet ?
Côté mise à jour asynchrone des données
Jusqu'ici, je ne sais pas si c'est "bien fait" ou si on peut mieux faire, mais tout marche bien.
Ce que je souhaite faire maintenant, c'est qu'un Service se connecte de temps en temps à un serveur web pour savoir si l'application doit mettre à jour sa base SQLite; et si c'est le cas, il faut mettre à jour la table.
La vérification est totalement transparente, pas besoin d’embêter l'utilisateur pour simplement demander à un serveur de dire "oui ou non" ^^
Si le serveur répond non, le service se termine, la prochaine vérification se fera quelques jours plus tard.
Si le serveur répond oui, c'est que les données ont changé : je suis obligé de vider la table, et de faire une insertion de masse.
La raison principale de cette "vidange" est que le serveur est incapable de me dire "par rapport à ta version, il faut que tu insert ça ça et ça, que tu update ce truc, et que tu delete ces machins". Il ne peut que me dire "insert tout çaaaaaaaaaaaaaaaaaaaaa".
Donc si une donnée est supprimée du serveur, ou modifiée, je n'ai pas trouvée d'autre solutions que de vider la table SQLite sur Android pour prendre en compte la modification.
Q2 : Est-ce que vous voyez un autre moyen que de vider et de tout ré-insérer ?
Q2 bis : A tout hasard, est-ce qu'il existe un petit framework/outils/truc permettant de rendre plus intelligent le serveur ?
Le service étant asynchrone, et la durée des traitements pouvant varier, il est impossible de prévoir quand les insertions dans la base vont être effectué.
Cela peut donc être problématique si l'application modifie déjà des données, ou si l'utilisateur filtres la liste (select).
Pour commencer à contrer ce problème, j'ai d'abord mis en place un singleton (voir code au dessus) pour l'accès à la base de données.
Vu que ce sont des insertions de masses, j'utilise une seule transaction + requête préparée pour améliorer les performances.
Les transactions SQLite sont en mode "exclusive" sur Android donc, lorsqu'une transaction démarre, toutes les autres requêtes sont bloquées; que ça soit une modification (update, insert, delete) ou un simple select. Il ne devrait donc pas y avoir de données "corrompues".
L'insertion de la mise à jour durant quelques secondes, l'utilisateur peut par contre être bloqué lorsqu'il utilise la base de données (s'il filtre la liste par exemple) et il devra attendre la fin de la transaction.
Alors du coup, pour contrer ce problème qui contrait déjà un problème (faut suivre hein :D), j'ai créé une seconde table identique qui n'est utilisée que par le Service. De plus, j'appelle la méthode yieldIfContendedSafely pour éviter que cette transaction soit bloquante. L'insertion dans cette table ne pose ainsi aucun problème et ne verouille donc jamais la base de données (utilisateur non bloqué).
Ceci étant fait, il faut que je transfère les données de cette table dans l'originale pour que ça puisse être utilisable par l'application. Pour cela, je créé manuellement une transaction qui engloble la "vidange" de la table principale puis la requête "INSERT INTO table_principale SELECT * FROM table_secondaire". Cette transaction est volontairement bloquante car il faut protéger cette section critique, mais c'est extrèmement rapide (moins de 0,5 secondes sur émulateur, à la place de 6 secondes pour l'insertion depuis le JSON).
Avec tout ce système, la mise à jour se fait totalement en arrière plan, et l'utilisateur n'est pas bloqué (peut-être une fraction de seconde lors de l'INSERT SELECT, mais ce sera surement rare/négligeable). Je pense utiliser le système de notification pour prévenir l'utilisateur lorsque la mise à jour commande (insert dans table secondaire), et lorsque la mise à jour est terminée.
Q3 : En espérant que vous ayez réussi à comprendre mon explication, qu'en pensez-vous ? Avez-vous des suggestions, des améliorations... ou même un changement radical à y apporter ?
Q4 : Si l'utilisateur est sur la listeview lorsque la mise à jour est terminée (et que la table secondaire a bien été insérée dans la table principale), la listview n'est pas rafraîchie : il faut quitter l'activité (retour Menu) puis revenir sur la listview. Je suppose que c'est dû au fait que le Cursor utilisé par l'adapter reste inchangé. Comment dire à l'adapter/au Cursor que le contenu de la base a changé, et qu'il faut se "réactualiser" ?
Côté Service
Pour l'instant, j'utilise une AsyncTask que je lance manuellement à différent endroits pour effectuer des tests sur la mise à jour. De plus, je n'ai jamais utilisé de Service et il y a encore trop de zones floues pour moi. J'ai donc quelques questions spécifiques aux Services Android :
Q5 : Tout d'abord, par rapport à mes besoins, je n'arrive pas à déterminer s'il me faut un LocalService ou un RemoteService.
Il ne faut pas qu'il tourne dans le thread UI, mais il n'a pas non plus besoin d'être utilisé par d'autres applications...
Q6 : Pour ce qui est du "bind", je pense que je n'en ai pas besoin car le Service ne dépend pas des activités, et l'inverse est vrai aussi. De plus, le service s'arrête lui-même. Est-ce que j'ai bon, ou est-ce qu'il y a une raison que je ne vois pas qui m'obligerait à "binder" le service ?
Q7 : Est-ce qu'on peut continuer d'éxécuter un Service lorsque l'application est terminée ? Est-ce valable autant pour un RemoteService qu'un LocalService ?
Q8 : Si oui, lorsque l'application est relancée, est-ce qu'il existe un moyen de savoir si mon service est exécuté (ou non) afin d'éviter de le relancer deux fois ?
Un grand merci d'avance pour votre aide (j'espère que je n'ai pas trop abusé avec la longueur et les questions).
Si un point semble flou, n'hésitez pas à me demander plus de détails.