Source Code Explanation
While the other good answers on here have correctly pointed out that this potential issue is documented in SharedPreferences.getStringSet(), basically "Don't modify the returned Set because the behavior isn't guaranteed", I'd like to actually contribute the source code that causes this problem/behavior for anyone that wants to dive deeper.
Taking a look at SharedPreferencesImpl (source code as of Android Pie) we can see that in SharedPreferencesImpl.commitToMemory()
there is a comparison that occurs between the original value (a Set<String>
in our case) and the newly modified value:
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);
}
So basically what's happening here is that when you try to write your changes to file this code will loop through your modified/added key/value pairs and check if they already exist, and will only write them to file if they don't or are different from the existing value that was read into memory.
The key line to pay attention to here is if (existingValue != null && existingValue.equals(v))
. You're new value will only be written to disk if existingValue
is null
(doesn't already exist) or if existingValue
's contents are different from the new value's contents.
This the the crux of the issue. existingValue
is read from memory. The SharedPreferences file that you are trying to modify is read into memory and stored as Map<String, Object> mMap;
(later copied into mapToWriteToDisk
each time you try to write to file). When you call getStringSet()
you get back a Set
from this in-memory Map. If you then add a value to this same Set
instance, you are modifying the in-memory Map. Then when you call editor.putStringSet()
and try to commit, commitToMemory()
gets executed, and the comparison line tries to compare your newly modified value, v
, to existingValue
which is basically the same in-memory Set
as the one you've just modified. The object instances are different, because the Set
s have been copied in various places, but the contents are identical.
So you're trying to compare your new data to your old data, but you've already unintentionally updated your old data by directly modifying that Set
instance. Thus your new data will not be written to file.
But why are the values stored initially but disappear after the app is killed?
As the OP stated, it seems as if the values are stored while you're testing the app, but then the new values disappear after you kill the app process and restart it. This is because while the app is running and you're adding values, you're still adding the values to the in-memory Set
structure, and when you call getStringSet()
you're getting back this same in-memory Set
. All your values are there and it looks like it's working. But after you kill the app, this in-memory structure is destroyed along with all the new values since they were never written to file.
Solution
As others have stated, just avoid modifying the in-memory structure, because you're basically causing a side-effect. So when you call getStringSet()
and want to reuse the contents as a starting point, just copy the contents into a different Set
instance instead of directly modifying it: new HashSet<>(getPrefs().getStringSet())
. Now when the comparison happens, the in-memory existingValue
will actually be different from your modified value v
.