Shared preferences onPreference change listener

from the CommonsWare Community archives

At August 21, 2019, 9:13pm, root-ansh asked:

So I am building an app with multiple fragments, activities and WorkManagers. Since a very little data has to be persisted, I am using shared preferences for this. Since this shared preference is being used by multiple classes and services, I thought having a single PrefHandler class would be simpler to test, Something like this:


public class PrefUserDetails {
    private SharedPreferences prefObj;
    private static final String PREF_NAME = "basicinfo";
    private interface KEYS {
        String KEY_GENDER = "basicinfo_gender";
        String KEY_WEIGHT = "basicinfo_weight";
        ....
    }
    public interface Defaults {
        String GENDER = UserGender.MALE.name();
        ...
    }
public enum UserGender {MALE, FEMALE}


    public PrefUserDetails(Context ctx) {
        this.prefObj = ctx.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
    }

    public UserGender getGender() {
        String gen = this.prefObj.getString(KEYS.KEY_GENDER, Defaults.GENDER);
        return UserGender.valueOf(gen);
    }
    public int getWeight() {
        return this.prefObj.getInt(KEYS.KEY_WEIGHT, Defaults.WEIGHT);
    }
    public void setGender(@NonNull UserGender gender) {
        this.prefObj.edit().putString(KEYS.KEY_GENDER, gender.name()).apply();
    }
    public void setWeight(int weight) {
        this.prefObj.edit().putInt(KEYS.KEY_WEIGHT, weight).apply();
    }
    public void registerPrefListener(SharedPreferences.OnSharedPreferenceChangeListener listener) {
        this.prefObj.registerOnSharedPreferenceChangeListener(listener);
    }
    public void unRegisterPrefListener(SharedPreferences.OnSharedPreferenceChangeListener listener) {
        this.prefObj.unregisterOnSharedPreferenceChangeListener(listener);
    }
    
}

i.e keeping original preference object private while exposing functions to get/set data.
My confusion is regarding SharedPreferences.OnPrefChangeListener. I initially added:

 public void registerPrefListener(SharedPreferences.OnSharedPreferenceChangeListener listener) {
        this.prefObj.registerOnSharedPreferenceChangeListener(listener);
    }
    public void unRegisterPrefListener(SharedPreferences.OnSharedPreferenceChangeListener listener) {
        this.prefObj.unregisterOnSharedPreferenceChangeListener(listener);
    }

but later i realised that this preferenceListener calls the function onSharedPreferenceChanged(SharedPreferences somePref, String s , i.e exposing some instance of shared preferences. Now my ui can use it via somePref.get(kry,Defaultvalue), but that also cause 2 problems:

  1. My Keys interface is private
  2. My whole point of using PrefBasicInfo for getting/setting data gets moot.

So i was wondering, instead of using somePref.get(kry,Defaultvalue) , can i use myPrefHandlerClassObj.getGender(...) on everytime the preferences are updated (i.e when onSharedPreferenceChanged(somePref,String s) is triggerred ? Is the original preference object also updated at the same time?


At August 21, 2019, 9:34pm, mmurphy replied:

I don’t see how, with the methods as you have defined them.

There is only one SharedPreferences object, for any given set of preferences. getSharedPreferences() for PREF_NAME will return the same object, no matter how many times you call it.

All that being said, I would use the repository pattern. Basically, have your PrefUserDetails be a singleton. Remove the register/unregister methods, replacing them with a reactive API (e.g., RxJava, LiveData, Kotlin coroutines) instead, where your repository publishes updates as the data changes.

I cover this repository architecture both in Elements of Android Jetpack and Exploring Android. In Exploring Android I even have a preference-backed repository, though in my case I did not need a reactive API, just an asynchronous one (and it’s in Kotlin).


At August 21, 2019, 9:52pm, root-ansh replied:

i mean, something like this:

pref=new PrefUserDetails(root.getContext()); //a global object in my fragment
prefListener = new OnSharedPreferenceChangeListener() { // a global object
            @Override
            public void onSharedPreferenceChanged(SharedPreferences prefRecieved, String s) { //ignoring the  recieved pref

                textViewGender.setText(pref.getGender().name());
                textViewTime.setText(pref.getSleepTime());
                ...
                // other ui getting updated using original global pref class object and completely
                // ignoring recieved Shared Preferences, as i guess my class's functions can 
                //  also be used to get/post live updates?
            }
        };
        pref.setListener(prefListener);

also, i checked the link. you have used coroutines, Do you have any examples involving livedata lying around? I have yet to learn about coroutines


At August 21, 2019, 10:43pm, mmurphy replied:

That’s what you said that you did not want to do — or, at least, that is how I interpreted this:

There is nothing stopping you from writing your code that way. However:

I have several examples in Elements of Android Jetpack of repositories using Java:

None use SharedPreferences as a data store. They use Room, Retrofit, content from ACTION_OPEN_DOCUMENT, etc.


At August 21, 2019, 10:52pm, root-ansh replied:

My main concern was mainly regarding this onSharedPreferenceChanged trigger and i ignoring the received preferences( and hiding access to Keys interface too, i guess)
Like does this trigger happen after or before the data getting permanently saved in the preferences? Because if the trigger is happening before data getting permanently saved, then this received shared preference might be my only possible way to access changes for a period of time(until changes gets permanent).
But if this trigger happens after changes are made permanent, then ignoring this will not cause in issue and i can simply access the preferences with my original instances.

i think its more of a “test it yourself” kind of question, but i was being a little curious about the internal working of this api


At August 21, 2019, 11:19pm, mmurphy replied:

Or, you could just call getString(), getInt(), etc. on the SharedPreferences. The exact timing of the persistence should not matter to you.

Bear in mind that SharedPreferences are cached in memory, both the object and the preference values themselves. When your listener is called, the in-memory SharedPreferences knows about the new values. However, those new values might not have been saved to disk yet. That is an implementation detail, one that might vary by Android OS version.


At August 22, 2019, 12:33am, mmurphy replied:

BTW, I would like to apologize if I have sounded harsh in these messages. I did not sleep well last night, and it’s been a long day.

The simplest way to think of SharedPreferences is: it is a HashMap that knows how to save itself to disk and knows how to tell listeners about changes.

So, when you call commit() or apply() on the Editor, the SharedPreferences itself is updated immediately. Whether the data is saved to disk immediately depends on whether you use commit() (saved immediately) or apply() (saved soon). If you use apply(), the exact timing of the disk I/O should not matter to you, as you are working off of the in-memory SharedPreferences. If you do care about the timing of that disk I/O, then SharedPreferences may not be the right data storage option for you — use a database or your own file.


At August 22, 2019, 8:07am, root-ansh replied:

haha you never sound harsh, i was already satisfied with your previous replies, thank you for your continuous support :slight_smile: . I don’t have time to test this, but i guess your word about callback being an OS dependent( and maybe manufacturer dependent?) might be correct. I have currently left the keys and defaults in a Utility file and doing all the insertion/deletion in the activities. I will try to make PreferencesManager an independent, testable module later, (and if not possible, i will go with room) but i think your way of using a livedata for this might just work.