This question already has an answer here:
-
Saving an entire Bundle to SharedPreferences
2 answers
I've gone to great lengths to make all of the data for my Android game fit into a savedInstanceState Bundle. There's a lot of data altogether, including many Parcelable objects. This ensures that when the app is paused or the orientation changes, no data gets lost by the Activity being recreated.
However, I have just recently discovered that a savedInstanceState bundle is apparently NOT appropriate for long-term storage. So I'm looking for a way that I can adapt my existing save method to work as a long-term solution, so that the game state can always be restored.
I have heard of 2 solutions so far:
1) Use the savedInstanceState bundle for orientation changes, but also incorporate SharedPrefs for when the app needs to be shut down completely.
This seems incredibly counter-productive, as it uses 2 different completely methods to do basically the same thing. Also, since my savedInstanceState Bundle uses Parcelable objects, I would have to give each of those objects another method to enable them to be written to SharedPrefs. Essentially LOTS of duplicated and difficult-to-manage code.
2) Serialize the savedInstanceState Bundle and write it directly to a file.
I am open to this, but I don't actually know how to go about doing it. However, I'm still holding onto the hope that there may be a better solution, as I've heard that serialization in Android is "comically / unusably slow".
I would be extremely grateful if someone could provide me with a solution to this.
Funny, this week, the issue 47 of Android Weekly unleashed this library : android complex preferences.
It should fit for you.
I have now come up with my own solution to this problem, which is a semi-automatic means of saving Bundles to SharedPreferences. I say semi-automatic because, although saving the Bundle requires only one method, retrieving the data again and turning it back into a Bundle takes some work.
Here is the code to save the Bundle:
SharedPreferences save = getSharedPreferences(SAVE, MODE_PRIVATE);
Editor ed = save.edit();
saveBundle(ed, "", gameState);
/**
* Manually save a Bundle object to SharedPreferences.
* @param ed
* @param header
* @param gameState
*/
private void saveBundle(Editor ed, String header, Bundle gameState) {
Set<String> keySet = gameState.keySet();
Iterator<String> it = keySet.iterator();
while (it.hasNext()){
key = it.next();
o = gameState.get(key);
if (o == null){
ed.remove(header + key);
} else if (o instanceof Integer){
ed.putInt(header + key, (Integer) o);
} else if (o instanceof Long){
ed.putLong(header + key, (Long) o);
} else if (o instanceof Boolean){
ed.putBoolean(header + key, (Boolean) o);
} else if (o instanceof CharSequence){
ed.putString(header + key, ((CharSequence) o).toString());
} else if (o instanceof Bundle){
saveBundle(header + key, ((Bundle) o));
}
}
ed.commit();
}
Note that I have only written cases for the types I needed, but this should be easily adaptable if you have Bundles that also include other types.
This method will recursively save other Bundle objects stored inside the given Bundle. However, it will not work for Parcelable objects, so I had to alter my Parcelable objects to make them store themselves into a Bundle instead. Since Parcels and Bundles are pretty similar, this wasn't too hard. I think Bundles may also be slightly slower than Parcels, unfortunately.
I then wrote constructors in all of my previously-Parcelable objects to enable them to re-Bundle themselves from the data stored SharedPreferences. It's easy enough to reconstruct the keys to the data you need. Say you have the following data structure:
Bundle b {
KEY_X -> int x;
KEY_Y -> Bundle y {
KEY_Z -> int z;
}
}
These will be saved to SharedPreferences as follows:
KEY_X -> x
KEY_YKEY_Z -> z
It may not be the prettiest method in the world, but it works, and it cost me much less code than the alternative, since now my onSaveInstanceState method and my onPause methods use the same technique.
I extended the answer from Dan with a function to recreate the Bundles automatically, and made the names less likely to clash.
private static final String SAVED_PREFS_BUNDLE_KEY_SEPARATOR = "§§";
/**
* Save a Bundle object to SharedPreferences.
*
* NOTE: The editor must be writable, and this function does not commit.
*
* @param editor SharedPreferences Editor
* @param key SharedPreferences key under which to store the bundle data. Note this key must
* not contain '§§' as it's used as a delimiter
* @param preferences Bundled preferences
*/
public static void savePreferencesBundle(SharedPreferences.Editor editor, String key, Bundle preferences) {
Set<String> keySet = preferences.keySet();
Iterator<String> it = keySet.iterator();
String prefKeyPrefix = key + SAVED_PREFS_BUNDLE_KEY_SEPARATOR;
while (it.hasNext()){
String bundleKey = it.next();
Object o = preferences.get(bundleKey);
if (o == null){
editor.remove(prefKeyPrefix + bundleKey);
} else if (o instanceof Integer){
editor.putInt(prefKeyPrefix + bundleKey, (Integer) o);
} else if (o instanceof Long){
editor.putLong(prefKeyPrefix + bundleKey, (Long) o);
} else if (o instanceof Boolean){
editor.putBoolean(prefKeyPrefix + bundleKey, (Boolean) o);
} else if (o instanceof CharSequence){
editor.putString(prefKeyPrefix + bundleKey, ((CharSequence) o).toString());
} else if (o instanceof Bundle){
savePreferencesBundle(editor, prefKeyPrefix + bundleKey, ((Bundle) o));
}
}
}
/**
* Load a Bundle object from SharedPreferences.
* (that was previously stored using savePreferencesBundle())
*
* NOTE: The editor must be writable, and this function does not commit.
*
* @param sharedPreferences SharedPreferences
* @param key SharedPreferences key under which to store the bundle data. Note this key must
* not contain '§§' as it's used as a delimiter
*
* @return bundle loaded from SharedPreferences
*/
public static Bundle loadPreferencesBundle(SharedPreferences sharedPreferences, String key) {
Bundle bundle = new Bundle();
Map<String, ?> all = sharedPreferences.getAll();
Iterator<String> it = all.keySet().iterator();
String prefKeyPrefix = key + SAVED_PREFS_BUNDLE_KEY_SEPARATOR;
Set<String> subBundleKeys = new HashSet<String>();
while (it.hasNext()) {
String prefKey = it.next();
if (prefKey.startsWith(prefKeyPrefix)) {
String bundleKey = StringUtils.removeStart(prefKey, prefKeyPrefix);
if (!bundleKey.contains(SAVED_PREFS_BUNDLE_KEY_SEPARATOR)) {
Object o = all.get(prefKey);
if (o == null) {
// Ignore null keys
} else if (o instanceof Integer) {
bundle.putInt(bundleKey, (Integer) o);
} else if (o instanceof Long) {
bundle.putLong(bundleKey, (Long) o);
} else if (o instanceof Boolean) {
bundle.putBoolean(bundleKey, (Boolean) o);
} else if (o instanceof CharSequence) {
bundle.putString(bundleKey, ((CharSequence) o).toString());
}
}
else {
// Key is for a sub bundle
String subBundleKey = StringUtils.substringBefore(bundleKey, SAVED_PREFS_BUNDLE_KEY_SEPARATOR);
subBundleKeys.add(subBundleKey);
}
}
else {
// Key is not related to this bundle.
}
}
// Recursively process the sub-bundles
for (String subBundleKey : subBundleKeys) {
Bundle subBundle = loadPreferencesBundle(sharedPreferences, prefKeyPrefix + subBundleKey);
bundle.putBundle(subBundleKey, subBundle);
}
return bundle;
}