SharedPreferences.onSharedPreferenceChangeListener not being called consistently
Asked Answered
T

8

296

I'm registering a preference change listener like this (in the onCreate() of my main activity):

SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);

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

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

The trouble is, the listener is not always called. It works for the first few times a preference is changed, and then it is no longer called until I uninstall and reinstall the app. No amount of restarting the application seems to fix it.

I found a mailing list thread reporting the same problem, but no one really answered him. What am I doing wrong?

Tetrahedral answered 30/3, 2010 at 4:55 Comment(0)
A
682

This is a sneaky one. SharedPreferences keeps listeners in a WeakHashMap. This means that you cannot use an anonymous inner class as a listener, as it will become the target of garbage collection as soon as you leave the current scope. It will work at first, but eventually, will get garbage collected, removed from the WeakHashMap and stop working.

Keep a reference to the listener in a field of your class and you will be OK, provided your class instance is not destroyed.

i.e. instead of:

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

do this:

// 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);

The reason unregistering in the onDestroy method fixes the problem is because to do that you had to save the listener in a field, therefore preventing the issue. It's the saving the listener in a field that fixes the problem, not the unregistering in onDestroy.

UPDATE: The Android docs have been updated with warnings about this behavior. So, oddball behavior remains. But now it's documented.

Abraham answered 23/6, 2010 at 18:2 Comment(7)
This was killing me, I thought I was losing my mind. Thank you for posting this solution!Cloyd
This does not work for me. Is there a way to make it work if the SharedPreference is used across two apps? I have one app making the change and another reacting to it. The only way I got it to work was to use the IPC model and send a broadcast from one APK to another.Winfredwinfrey
I have done the same as u answered @Abraham but after closing the app the listner is not working .I did not unregister the listner in onDestroy() method.Morita
No amount of restarting the application seems to fix it : could you comment on this part of the question ? Wouldn't onCreate() of the main activity be called again on restarting the app and therefore add the listener to the map ?Thrombophlebitis
Great answer, thank you. Definitely should be mentioned in the docs. code.google.com/p/android/issues/detail?id=48589Horodko
Thank you so much. Just got tricked by this one todaySwansdown
Thank you so much!!! Been sitting and scratching my head for HOURS about this! You're a lifesaver!!Argyle
F
25

this accepted answer is ok, as for me it is creating new instance each time the activity resumes

so how about keeping the reference to the listener within the activity

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

and in your onResume and onPause

@Override     
public void onResume() {
    super.onResume();          
    getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(listener);     
}

@Override     
public void onPause() {         
    super.onPause();          
    getPreferenceScreen().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(listener);

}

this will very similar to what you are doing except we are maintaining a hard reference.

Fourthly answered 25/8, 2011 at 4:45 Comment(5)
Why you used super.onResume() before getPreferenceScreen()... ?Shayna
@YoushaAleayoub, read about android.app.supernotcalledexception it is required by android implementation.Fourthly
what do you mean? using super.onResume() is required OR using it BEFORE getPreferenceScreen() is required? because I'm talking about the RIGHT place. cs.dartmouth.edu/~campbell/cs65/lecture05/lecture05.htmlShayna
I remember reading it here developer.android.com/training/basics/activity-lifecycle/…, see the comment in the code. but putting it in tail is logical. but till now I haven't faced any problems in this.Fourthly
Thanks a lot, everywhere else that I found onResume() and onPause() method they registered this and not listener, it caused error and I could solve my problem. Btw these two methods are public now, not protectedSidra
T
23

The accepted answer creates a SharedPreferenceChangeListener every time onResume is called. @Samuel solves it by making SharedPreferenceListener a member of the Activity class. But there's a third and a more straightforward solution that Google also uses in this codelab. Make your activity class implement the OnSharedPreferenceChangeListener interface and override onSharedPreferenceChanged in the Activity, effectively making the Activity itself 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);
    }
}
Titus answered 24/6, 2018 at 17:5 Comment(2)
exactly, this should be it. implement the interface, register in onStart, and unRegister in onStop.Fulbert
This official approach will not work in case you are starting a new activity, which will stop the current activity and unregister the listener, for instance, an intent action to select a file from another app. In this case, using onDestroy instead of onStop/onPause will do the job.Numerate
A
16

As this is the most detailed page for the topic I want to add my 50ct.

I had the problem that OnSharedPreferenceChangeListener wasn't called. My SharedPreferences are retrieved at the start of the main Activity by:

prefs = PreferenceManager.getDefaultSharedPreferences(this);

My PreferenceActivity code is short and does nothing except showing the preferences:

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

Every time the menu button is pressed I create the PreferenceActivity from the main Activity:

@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;
}

Note that registering the OnSharedPreferenceChangeListener needs to be done AFTER creating the PreferenceActivity in this case, else the Handler in the main Activity won't be called!!! It took me some sweet time to realize that...

Adz answered 29/12, 2011 at 12:42 Comment(0)
B
3

Kotlin Code for register SharedPreferenceChangeListener it detect when change will happening on the saved key :

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

you can put this code in onStart() , or somewhere else.. *Consider that you must use

 if(key=="YourKey")

or your codes inside "//Do Something " block will be run wrongly for every change that will happening in any other key in sharedPreferences

Backset answered 14/1, 2019 at 19:16 Comment(0)
N
2

So, I don't know if this would really help anyone though, it solved my issue. Even though I had implemented the OnSharedPreferenceChangeListener as stated by the accepted answer. Still, I had an inconsistency with the listener being called.

I came here to understand that the Android just sends it for garbage collection after some time. So, I looked over at my code. To my shame, I had not declared the listener GLOBALLY but instead inside the onCreateView. And that was because I listened to the Android Studio telling me to convert the listener to a local variable.

Nomen answered 21/8, 2018 at 1:9 Comment(1)
It won't tell you to convert it to a local variable if you register in OnCreate/OnStart/OnResume and unregister in OnDestroy/OnStop/OnPause, thus using it within two different methods. I could be wrong about this, but which methods you register/unregister in will depend on whether you want the listener to be active while another activity is opened over it. For example, if you have some kind of SettingsActivity for the user to change their username, and you want the change to be recognized by the MainActivity immediately, rather than returning the value to the MainActivity.Crosstie
T
0

It make sense that the listeners are kept in WeakHashMap.Because most of the time, developers prefer to writing the code like this.

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

This may seem not bad. But if the OnSharedPreferenceChangeListeners' container was not WeakHashMap, it would be very bad.If the above code was written in an Activity . Since you are using non-static (anonymous) inner class which will implicitly holds the reference of the enclosing instance. This will cause memory leak.

What's more, If you keep the listener as a field, you could use registerOnSharedPreferenceChangeListener at the start and call unregisterOnSharedPreferenceChangeListener in the end. But you can not access a local variable in a method out of it's scope. So you just have the opportunity to register but no chance to unregister the listener. Thus using WeakHashMap will resolve the problem. This is the way I recommend.

If you make the listener instance as a static field, It will avoid the memory leak caused by non-static inner class. But as the listeners could be multiple, It should be instance-related. This will reduce the cost of handling the onSharedPreferenceChanged callback.

Termitarium answered 27/11, 2014 at 12:10 Comment(0)
J
-3

While reading Word readable data shared by first app,we should

Replace

getSharedPreferences("PREF_NAME", Context.MODE_PRIVATE);

with

getSharedPreferences("PREF_NAME", Context.MODE_MULTI_PROCESS);

in second app to get updated value in second app.

But still it is not working...

Javierjavler answered 11/2, 2013 at 19:0 Comment(6)
Android doesn't support accessing SharedPreferences from multiple processes. Doing so causes concurrency issues, which can result in all preferences being lost. Also, MODE_MULTI_PROCESS is no longer supported.Sarpedon
@Sarpedon this answer is 3 years old, please so don't down-vote it, if its not working for you in latest versions of android. the time answer were written it was the best approach to do so.Javierjavler
No, that approach was never multi process safe, even when you wrote this answer.Sarpedon
As @Sarpedon states, correctly, shared prefs was never process safe. Also to shridutt kothari - if you don't like the downvotes remove your incorrect answer (it doesn't answer the OP's question anyway). However, If you'd still like to use shared prefs in a process safe way you need to create a process safe abstraction above it i.e. a ContentProvider, which is process safe, and still allows you to use shared preferences as the persisted storage mechanism., I've done this before, and for small datasets/preferences out performs sqlite by a fair margin.Hollowell
@MarkKeen By the way, I actually tried using content providers for this and the content providers tended to fail randomly in Production due to Android bugs, so these days I just use broadcasts to sync the configuration to secondary processes as needed.Sarpedon
@Sarpedon good to know - I did do some instrumentation tests around the Content Provider, and seemed to work, but as you say - when you get to a production environment with long term use odd things can happen!Hollowell

© 2022 - 2024 — McMap. All rights reserved.