24

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.

Dan
  • 1,198
  • 4
  • 17
  • 34
  • To serialize just look for a serialization class, it shouldn't be very hard to find. I haven't noticed any excruciating delay while using it – mango Dec 01 '12 at 15:58
  • The only information I can find tells me I need to implement Serializable - but Bundle doesn't implement this interface. – Dan Dec 02 '12 at 14:38
  • I recommend library https://github.com/iamironz/binaryprefs, It allows save data like standard java through implementation Persistable interface (Externalizable interface in jdk) – denis.sugakov Oct 31 '17 at 13:23

3 Answers3

16

Funny, this week, the issue 47 of Android Weekly unleashed this library : android complex preferences.

It should fit for you.

Snicolas
  • 37,840
  • 15
  • 114
  • 173
  • This seemed so promising, but alas, I can't make it work for anything. I've tried with various Bundle objects, including empty ones, and some simpler objects like Points, but still no luck. It either complains about a "circular reference" when saving, or a "object storaged at key ___ is instanceof another game" when loading. This is driving me crazy... – Dan Dec 01 '12 at 17:47
  • Please post this as a separate question. I would be interested if you add a comment to my address so I can follow it. – Snicolas Dec 01 '12 at 19:19
  • 1
    Indeed it is just saving everything as JSon using GSon... Anyhow, my feeling is that your data might be inner classes of something. This would give you cycles very easily. Are your POJOs separate classes ? – Snicolas Dec 01 '12 at 19:30
  • Here's the new question: http://stackoverflow.com/questions/13665389/saving-objects-with-android-complexpreferences-gson Thanks for your help by the way. Also, where I wrote "instanceof another game" in my previous comment, that should have been "instanceof another class". – Dan Dec 02 '12 at 00:55
4

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.

Dan
  • 1,198
  • 4
  • 17
  • 34
  • how can we getBundle in this situation? Thanks – Nam Vu May 24 '13 at 07:17
  • 1
    I'm not sure what you mean exactly... Once the bundle has been saved to SharedPrefs, it can be retrieved like any other bundle. – Dan May 24 '13 at 21:00
  • what are key and o here. Form where do you pass these arguements – ekjyot Apr 24 '14 at 06:32
  • Can you pls also tell how to get bundle from shared prefernces – ekjyot Apr 24 '14 at 09:38
  • Sorry!! I've lost the original code but I've edited the post with my best guess of what it should look like. To retrieve the Bundle from SharedPreferences you have to manually re-create the keys, as described at the bottom of my post, and then use those keys to retrieve what you want. I'm starting to think that saving a Bundle to SharedPreferences is not a sensible idea after all :) – Dan Apr 24 '14 at 18:23
2

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;
}
Relefant
  • 928
  • 10
  • 10
  • https://commons.apache.org/proper/commons-lang/javadocs/api-2.6/src-html/org/apache/commons/lang/StringUtils.html to get what StringUtils funcs are doing (not included in android) – James Alvarez Feb 26 '15 at 15:10