SharedPreferences.onSharedPreferenceChangeListener n'est pas appelé de manière cohérente

267

J'enregistre un écouteur de changement de préférence comme celui-ci (dans le onCreate()de mon activité principale):

SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);

prefs.registerOnSharedPreferenceChangeListener(
   new SharedPreferences.OnSharedPreferenceChangeListener() {
       public void onSharedPreferenceChanged(
         SharedPreferences prefs, String key) {

         System.out.println(key);
       }
});

Le problème est que l'auditeur n'est pas toujours appelé. Cela fonctionne pour les premières fois une préférence est modifiée, puis elle n'est plus appelée jusqu'à ce que je désinstalle et réinstalle l'application. Aucun redémarrage de l'application ne semble le résoudre.

J'ai trouvé un fil de liste de diffusion signalant le même problème, mais personne ne lui a vraiment répondu. Qu'est-ce que je fais mal?

synique
la source

Réponses:

612

Ceci est sournois. SharedPreferences conserve les auditeurs dans un WeakHashMap. Cela signifie que vous ne pouvez pas utiliser une classe interne anonyme comme écouteur, car elle deviendra la cible de la récupération de place dès que vous quitterez la portée actuelle. Cela fonctionnera au début, mais finira par récupérer les ordures, les retirer du WeakHashMap et cesser de fonctionner.

Gardez une référence à l'auditeur dans un champ de votre classe et tout ira bien, à condition que votre instance de classe ne soit pas détruite.

c'est-à-dire au lieu de:

prefs.registerOnSharedPreferenceChangeListener(
  new SharedPreferences.OnSharedPreferenceChangeListener() {
  public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
    // Implementation
  }
});

faites ceci:

// Use instance field for listener
// It will not be gc'd as long as this instance is kept referenced
listener = new SharedPreferences.OnSharedPreferenceChangeListener() {
  public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
    // Implementation
  }
};

prefs.registerOnSharedPreferenceChangeListener(listener);

La raison de la désinscription dans la méthode onDestroy résout le problème car, pour ce faire, vous deviez enregistrer l'écouteur dans un champ, évitant ainsi le problème. C'est l'enregistrement de l'écouteur dans un champ qui résout le problème, pas la désinscription dans onDestroy.

MISE À JOUR : Les documents Android ont été mis à jour avec des avertissements sur ce comportement. Ainsi, le comportement bizarre reste. Mais maintenant, c'est documenté.

Blanka
la source
20
Cela me tuait, je pensais que je perdais la tête. Merci d'avoir posté cette solution!
Brad Hein
10
Ce message est ÉNORME, merci beaucoup, cela aurait pu me coûter des heures de débogage impossible!
Kevin Gaudin
Génial, c'est juste ce dont j'avais besoin. Bonne explication aussi!
stealthcopter
Cela m'a déjà mordu et je ne savais pas ce qui se passait jusqu'à la lecture de ce post. Je vous remercie! Boo, Android!
Nakedible
5
Excellente réponse, merci. Certainement devrait être mentionné dans les documents. code.google.com/p/android/issues/detail?id=48589
Andrey Chernih
16

cette réponse acceptée est correcte, car pour moi, elle crée une nouvelle instance chaque reprise de l'activité

alors que diriez-vous de garder la référence à l'auditeur dans l'activité

OnSharedPreferenceChangeListener myPrefListner = new OnSharedPreferenceChangeListener(){
      public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
         // your stuff
      }
};

et dans votre onResume et onPause

@Override     
protected void onResume() {
    super.onResume();          
    getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(myPrefListner);     
}



@Override     
protected void onPause() {         
    super.onPause();          
    getPreferenceScreen().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(myPrefListner);

}

cela sera très similaire à ce que vous faites, sauf que nous conservons une référence stricte.

Samuel
la source
Pourquoi avez-vous utilisé super.onResume()avant getPreferenceScreen()...?
Yousha Aleayoub
@YoushaAleayoub, lisez les informations sur android.app.supernotcalledexception requises par l'implémentation Android.
Samuel
Que voulez-vous dire? utiliser super.onResume()est requis OU l'utiliser AVANT getPreferenceScreen()est requis? parce que je parle du bon endroit. cs.dartmouth.edu/~campbell/cs65/lecture05/lecture05.html
Yousha Aleayoub
Je me souviens l'avoir lu ici developer.android.com/training/basics/activity-lifecycle/… , voir le commentaire dans le code. mais le mettre en queue est logique. mais jusqu'à présent, je n'ai rencontré aucun problème.
Samuel
Merci beaucoup, partout ailleurs que j'ai trouvé sur la méthode onResume () et onPause () qu'ils ont enregistrée thiset non listener, cela a provoqué une erreur et j'ai pu résoudre mon problème. Btw ces deux méthodes sont publiques maintenant, non protégées
Nicolas
16

Comme c'est la page la plus détaillée du sujet, je veux ajouter mon 50ct.

J'ai eu le problème que OnSharedPreferenceChangeListener n'a pas été appelé. Mes préférences partagées sont récupérées au début de l'activité principale par:

prefs = PreferenceManager.getDefaultSharedPreferences(this);

Mon code PreferenceActivity est court et ne fait rien d'autre que d'afficher les préférences:

public class Preferences extends PreferenceActivity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // load the XML preferences file
        addPreferencesFromResource(R.xml.preferences);
    }
}

Chaque fois que le bouton de menu est enfoncé, je crée la PreferenceActivity à partir de l'activité principale:

@Override
public boolean onPrepareOptionsMenu(Menu menu) {
    super.onCreateOptionsMenu(menu);
    //start Preference activity to show preferences on screen
    startActivity(new Intent(this, Preferences.class));
    //hook into sharedPreferences. THIS NEEDS TO BE DONE AFTER CREATING THE ACTIVITY!!!
    prefs.registerOnSharedPreferenceChangeListener(this);
    return false;
}

Notez que l'enregistrement de OnSharedPreferenceChangeListener doit être effectué APRÈS la création de PreferenceActivity dans ce cas, sinon le gestionnaire de l'activité principale ne sera pas appelé !!! Cela m'a pris un peu de temps pour réaliser que ...

Bim
la source
9

La réponse acceptée crée un appel à SharedPreferenceChangeListenerchaque fois onResume. @Samuel le résout en devenant SharedPreferenceListenermembre de la classe Activity. Mais il y a une troisième et une solution plus simple que Google utilise également dans ce codelab . Faites en sorte que votre classe d'activité implémente l' OnSharedPreferenceChangeListenerinterface et écrase onSharedPreferenceChangedl'activité, ce qui rend l'activité elle-même a SharedPreferenceListener.

public class MainActivity extends Activity implements SharedPreferences.OnSharedPreferenceChangeListener {

    @Override
    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String s) {

    }

    @Override
    protected void onStart() {
        super.onStart();
        PreferenceManager.getDefaultSharedPreferences(this)
                .registerOnSharedPreferenceChangeListener(this);
    }

    @Override
    protected void onStop() {
        super.onStop();
        PreferenceManager.getDefaultSharedPreferences(this)
                .unregisterOnSharedPreferenceChangeListener(this);
    }
}
PrashanD
la source
1
exactement, ça devrait être ça. implémentez l'interface, enregistrez-vous dans onStart et annulez l'enregistrement dans onStop.
Junaed
2

Code Kotlin pour enregistrer SharedPreferenceChangeListener, il détecte quand le changement se produira sur la clé enregistrée:

  PreferenceManager.getDefaultSharedPreferences(this)
        .registerOnSharedPreferenceChangeListener { sharedPreferences, key ->
            if(key=="language") {
                //Do Something 
            }
        }

vous pouvez mettre ce code dans onStart (), ou ailleurs .. * Considérez que vous devez utiliser

 if(key=="YourKey")

ou vos codes dans le bloc "// Do Something" seront mal exécutés pour chaque changement qui se produira dans n'importe quelle autre clé de sharedPreferences

Hamed Jaliliani
la source
1

Donc, je ne sais pas si cela aiderait vraiment quelqu'un, cela a résolu mon problème. Même si j'avais mis en œuvre le OnSharedPreferenceChangeListenercomme indiqué par la réponse acceptée . Pourtant, j'avais une incohérence avec l'auditeur appelé.

Je suis venu ici pour comprendre que l'Android l'envoie juste pour la collecte des déchets après un certain temps. Alors, j'ai regardé mon code. À ma honte, je n'avais pas déclaré l'auditeur GLOBALEMENT mais plutôt à l'intérieur du onCreateView. Et c'est parce que j'ai écouté Android Studio me dire de convertir l'auditeur en une variable locale.

Asghar Musani
la source
0

Il est logique que les écouteurs soient conservés dans WeakHashMap.Parce que la plupart du temps, les développeurs préfèrent écrire le code comme ceci.

PreferenceManager.getDefaultSharedPreferences(getApplicationContext()).registerOnSharedPreferenceChangeListener(
    new OnSharedPreferenceChangeListener() {
    @Override
    public void onSharedPreferenceChanged(
        SharedPreferences sharedPreferences, String key) {
        Log.i(LOGTAG, "testOnSharedPreferenceChangedWrong key =" + key);
    }
});

Cela peut sembler pas mal. Mais si le conteneur d'OnSharedPreferenceChangeListeners n'était pas WeakHashMap, ce serait très mauvais.Si le code ci-dessus était écrit dans une activité. Puisque vous utilisez une classe interne non statique (anonyme) qui contiendra implicitement la référence de l'instance englobante. Cela entraînera une fuite de mémoire.

De plus, si vous conservez l'écouteur en tant que champ, vous pouvez utiliser registerOnSharedPreferenceChangeListener au début et appeler unregisterOnSharedPreferenceChangeListener à la fin. Mais vous ne pouvez pas accéder à une variable locale dans une méthode hors de sa portée. Vous avez donc juste la possibilité de vous inscrire mais aucune chance de désinscrire l'auditeur. Ainsi, l'utilisation de WeakHashMap résoudra le problème. C'est ainsi que je recommande.

Si vous créez l'instance d'écouteur en tant que champ statique, cela évitera la fuite de mémoire causée par la classe interne non statique. Mais comme les écouteurs peuvent être multiples, cela devrait être lié à l'instance. Cela réduira le coût de la gestion du rappel onSharedPreferenceChanged .

androidyue
la source
-3

Lors de la lecture de données lisibles par Word partagées par la première application, nous devrions

Remplacer

getSharedPreferences("PREF_NAME", Context.MODE_PRIVATE);

avec

getSharedPreferences("PREF_NAME", Context.MODE_MULTI_PROCESS);

dans la deuxième application pour obtenir une valeur mise à jour dans la deuxième application.

Mais ça ne marche toujours pas ...

shridutt kothari
la source
Android ne prend pas en charge l'accès à SharedPreferences à partir de plusieurs processus. Cela entraîne des problèmes de simultanéité, ce qui peut entraîner la perte de toutes les préférences. De plus, MODE_MULTI_PROCESS n'est plus pris en charge.
Sam
@Sam cette réponse a 3 ans, alors ne la votez pas, si elle ne fonctionne pas pour vous dans les dernières versions d'Android. une fois la réponse écrite, c'était la meilleure approche pour le faire.
shridutt kothari
1
Non, cette approche n'a jamais été sécurisée par plusieurs processus, même lorsque vous avez écrit cette réponse.
Sam
Comme l'affirme @Sam, correctement, les préférences partagées n'ont jamais été sécurisées. Aussi à shridutt kothari - si vous n'aimez pas les downvotes, supprimez votre réponse incorrecte (cela ne répond pas de toute façon à la question du PO). Cependant, si vous souhaitez toujours utiliser les préférences partagées de manière sécurisée pour les processus, vous devez créer une abstraction sécurisée pour les processus au-dessus, c'est-à-dire un ContentProvider, qui est sécurisé pour les processus et vous permet toujours d'utiliser les préférences partagées comme mécanisme de stockage persistant. , Je l'ai déjà fait auparavant, et pour les petits ensembles de données / préférences, sqlite est plus performant.
Mark Keen
1
@MarkKeen Soit dit en passant, j'ai en fait essayé d'utiliser des fournisseurs de contenu pour cela et les fournisseurs de contenu avaient tendance à échouer de manière aléatoire dans la production en raison de bogues Android, donc ces jours-ci, j'utilise simplement des diffusions pour synchroniser la configuration avec les processus secondaires selon les besoins.
Sam