web-dev-qa-db-fra.com

Comportement incorrect lors de la tentative de stockage d'un jeu de chaînes à l'aide de SharedPreferences

J'essaie de stocker un ensemble de chaînes à l'aide de l'API SharedPreferences .

Set<String> s = sharedPrefs.getStringSet("key", new HashSet<String>());
s.add(new_element);

SharedPreferences.Editor editor = sharedPrefs.edit();
editor.putStringSet(s);
edit.commit()

La première fois que j'exécute le code ci-dessus, s est réglé sur la valeur par défaut (la fin juste créée HashSet) et il est stocké sans problème.

La deuxième fois et la prochaine fois que j'exécute ce code, un objet s est renvoyé avec le premier élément ajouté. Je peux ajouter l'élément, et pendant l'exécution du programme, il est apparemment stocké dans le SharedPreferences, mais lorsque le programme est tué, le SharedPreferences relit à partir de son stockage persistant et les nouvelles valeurs sont perdu.

Comment le second et les éléments suivants peuvent-ils être stockés pour ne pas se perdre?

62
JoseLSegura

Ce "problème" est documenté sur SharedPreferences.getStringSet .

Le SharedPreferences.getStringSet renvoie une référence de l'objet HashSet stocké à l'intérieur de SharedPreferences. Lorsque vous ajoutez des éléments à cet objet, ils sont en fait ajoutés à l'intérieur du SharedPreferences.

C'est correct, mais le problème survient lorsque vous essayez de le stocker: Android compare le HashSet modifié que vous essayez d'enregistrer en utilisant SharedPreferences.Editor.putStringSet avec celui actuellement stocké sur le SharedPreference, et les deux sont le même objet !!!

Une solution possible consiste à faire une copie du Set<String> retourné par l'objet SharedPreferences:

Set<String> s = new HashSet<String>(sharedPrefs.getStringSet("key", new HashSet<String>()));

Cela fait de s un objet différent, et les chaînes ajoutées à s ne seront pas ajoutées à l'ensemble stocké dans SharedPreferences.

Une autre solution de contournement qui fonctionnera consiste à utiliser le même SharedPreferences.Editor transaction pour stocker une autre préférence plus simple (comme un entier ou un booléen), la seule chose dont vous avez besoin est de forcer la valeur stockée à chaque transaction (par exemple, vous pouvez stocker la taille de l'ensemble de chaînes).

141
JoseLSegura

Ce comportement est documenté, il est donc par conception:

de getStringSet:

"Notez que vous ne devez pas modifier l'instance d'ensemble renvoyée par cet appel. La cohérence des données stockées n'est pas garantie si vous le faites, ni votre capacité à modifier l'instance du tout."

Et cela semble tout à fait raisonnable, surtout s'il est documenté dans l'API, sinon cette API devrait faire une copie à chaque accès. La raison de cette conception était donc probablement la performance. Je suppose qu'ils devraient faire en sorte que cette fonction renvoie le résultat enveloppé dans une instance de classe non modifiable, mais cela nécessite une fois de plus une allocation.

13
marcinj

Était à la recherche d'une solution pour le même problème, l'a résolu par:

1) Récupérez l'ensemble existant des préférences partagées

2) Faites-en une copie

3) Mettre à jour la copie

4) Enregistrez la copie

SharedPreferences.Editor editor = sharedPrefs.edit();
Set<String> oldSet = sharedPrefs.getStringSet("key", new HashSet<String>());

//make a copy, update it and save it
Set<String> newStrSet = new HashSet<String>();    
newStrSet.add(new_element);
newStrSet.addAll(oldSet);

editor.putStringSet("key",newStrSet); edit.commit();

Pourquoi

4
s-hunter

J'ai essayé toutes les réponses ci-dessus, aucune n'a fonctionné pour moi. J'ai donc fait les étapes suivantes

  1. avant d'ajouter un nouvel élément à la liste des anciennes préférences partagées, faites-en une copie
  2. appeler une méthode avec la copie ci-dessus comme paramètre de cette méthode.
  3. à l'intérieur de cette méthode, effacez les préférences partagées qui contiennent ces valeurs.
  4. ajoutez les valeurs présentes dans la copie à la préférence partagée effacée, il la traitera comme nouvelle.

    public static void addCalcsToSharedPrefSet(Context ctx,Set<String> favoriteCalcList) {
    
    ctx.getSharedPreferences(FAV_PREFERENCES, 0).edit().clear().commit();
    
    SharedPreferences sharedpreferences = ctx.getSharedPreferences(FAV_PREFERENCES, Context.MODE_PRIVATE);
    SharedPreferences.Editor editor = sharedpreferences.edit();
    editor.putStringSet(FAV_CALC_NAME, favoriteCalcList);
    editor.apply(); }
    

J'étais confronté à un problème avec les valeurs non persistantes, si je rouvrais l'application après avoir nettoyé l'application de l'arrière-plan, seul le premier élément ajouté à la liste était affiché.

0
Swapnil

Explication du code source

Alors que les autres bonnes réponses ici ont correctement souligné que ce problème potentiel est documenté dans SharedPreferences.getStringSet () , essentiellement "Ne modifiez pas l'ensemble renvoyé car le comportement n'est pas garanti", je Je voudrais réellement contribuer au code source qui cause ce problème/comportement pour quiconque souhaite approfondir.

En jetant un œil à SharedPreferencesImpl (code source à partir de Android Pie), nous pouvons voir que dans SharedPreferencesImpl.commitToMemory() il y a une comparaison qui se produit entre l'original (un Set<String> dans notre cas) et la valeur nouvellement modifiée:

private MemoryCommitResult commitToMemory() {
    // ... other code

    // mModified is a Map of all the key/values added through the various put*() methods.
    for (Map.Entry<String, Object> e : mModified.entrySet()) {
        String k = e.getKey();
        Object v = e.getValue();
        // ... other code

        // mapToWriteToDisk is a copy of the in-memory Map of our SharedPreference file's
        // key/value pairs.
        if (mapToWriteToDisk.containsKey(k)) {
            Object existingValue = mapToWriteToDisk.get(k);
            if (existingValue != null && existingValue.equals(v)) {
                continue;
            }
        }
        mapToWriteToDisk.put(k, v);
    }

Donc, fondamentalement, ce qui se passe ici, c'est que lorsque vous essayez d'écrire vos modifications dans le fichier, ce code passera en revue vos paires clé/valeur modifiées/ajoutées et vérifiera qu'elles existent déjà, et ne les écrira dans le fichier que si elles ne le sont pas ou ne le sont pas différente de la valeur existante qui a été lue en mémoire.

La ligne clé à suivre ici est if (existingValue != null && existingValue.equals(v)). Votre nouvelle valeur ne sera écrite sur le disque que si existingValue est null (n'existe pas déjà) ou si le contenu de existingValue est différent du contenu de la nouvelle valeur.

Ceci est le nœud du problème. existingValue est lu depuis la mémoire. Le fichier SharedPreferences que vous essayez de modifier est lu en mémoire et stocké sous Map<String, Object> mMap; (Plus tard copié dans mapToWriteToDisk chaque fois que vous essayez d'écrire dans un fichier). Lorsque vous appelez getStringSet(), vous obtenez un Set à partir de cette carte en mémoire. Si vous ajoutez ensuite une valeur à cette même instance Set, vous modifiez la carte en mémoire . Ensuite, lorsque vous appelez editor.putStringSet() et essayez de valider, commitToMemory() est exécuté et la ligne de comparaison essaie de comparer votre valeur nouvellement modifiée, v, à existingValue qui est essentiellement le même _ en mémoire Set que celui que vous venez de modifier. Les instances d'objet sont différentes, car les Set ont été copiés à divers endroits, mais le contenu est identique.

Donc vous essayez de comparer vos nouvelles données à vos anciennes données, mais vous avez déjà involontairement mis à jour vos anciennes données en modifiant directement ce Set instance. Ainsi, vos nouvelles données ne seront pas écrites dans un fichier.

Mais pourquoi les valeurs sont-elles stockées initialement mais disparaissent après la mort de l'application?

Comme l'OP l'a indiqué, il semble que les valeurs soient stockées pendant que vous testez l'application, mais les nouvelles valeurs disparaissent après avoir tué le processus d'application et redémarré. En effet, pendant que l'application est en cours d'exécution et que vous ajoutez des valeurs, vous ajoutez toujours les valeurs à la mémoire en mémoire Set structure, et lorsque vous appelez getStringSet() vous récupérez cette même mémoire Set. Toutes vos valeurs sont là et il semble que ça marche. Mais après avoir tué l'application, cette structure en mémoire est détruite avec toutes les nouvelles valeurs car elles n'ont jamais été écrites dans un fichier.

Solution

Comme d'autres l'ont dit, évitez simplement de modifier la structure en mémoire, car vous causez essentiellement un effet secondaire. Ainsi, lorsque vous appelez getStringSet() et que vous souhaitez réutiliser le contenu comme point de départ, copiez simplement le contenu dans une autre instance Set au lieu de le modifier directement: new HashSet<>(getPrefs().getStringSet()). Maintenant, lorsque la comparaison se produit, le existingValue en mémoire sera en fait différent de votre valeur modifiée v.

0
Tony Chan