21

I'd like to serialize a Bundle object, but can't seem to find a simple way of doing it. Using Parcel doesn't seem like an option, since I want to store the serialized data to file.

Any ideas on ways to do this?

The reason I want this is to save and restore the state of my activity, also when it's killed by the user. I already create a Bundle with the state I want to save in onSaveInstanceState. But android only keeps this Bundle when the activity is killed by the SYSTEM. When the user kills the activity, I need to store it myself. Hence i'd like to serialize and store it to file. Of course, if you have any other way of accomplishing the same thing, i'd be thankful for that too.

Edit: I decided to encode my state as a JSONObject instead of a Bundle. The JSON object can then be put in a Bundle as a Serializable, or stored to file. Probably not the most efficient way, but it's simple, and it seems to work ok.

hermo
  • 515
  • 1
  • 4
  • 13

4 Answers4

7

storing any Parcelable to a file is very easy:

FileOutputStream fos = context.openFileOutput(localFilename, Context.MODE_PRIVATE);
Parcel p = Parcel.obtain(); // i make an empty one here, but you can use yours
fos.write(p.marshall());
fos.flush();
fos.close();

enjoy!

reflog
  • 7,587
  • 1
  • 42
  • 47
  • 9
    Yes, i found that too. The problem is that there's no guarantee that you can unmarshall it again, say if the OS is updated and Parcel has changed. But if you can live with that then it's fine. – hermo Apr 09 '10 at 04:41
  • 14
    The data you retrieve from the method mashall() must not be placed in any kind of persistent storage (on local disk, across a network, etc). For that, you should use standard serialization or another kind of general serialization mechanism. The Parcel marshalled representation is highly optimized for local IPC, and as such does not attempt to maintain compatibility with data created in different versions of the platform. (http://developer.android.com/reference/android/os/Parcel.html#marshall()) – Oneiros Jun 11 '12 at 17:59
  • I suppose that you shouldn't transfer saved file to other devies, but probably it's ok if you are using it on single device (for saving temporary data, for example). – Dmitry Zaytsev Aug 24 '12 at 12:08
6

Convert it to SharedPreferences:

private void saveToPreferences(Bundle in) {
    Parcel parcel = Parcel.obtain();
    String serialized = null;
    try {
        in.writeToParcel(parcel, 0);

        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        IOUtils.write(parcel.marshall(), bos);

        serialized = Base64.encodeToString(bos.toByteArray(), 0);
    } catch (IOException e) {
        Log.e(getClass().getSimpleName(), e.toString(), e);
    } finally {
        parcel.recycle();
    }
    if (serialized != null) {
        SharedPreferences settings = getSharedPreferences(PREFS, 0);
        Editor editor = settings.edit();
        editor.putString("parcel", serialized);
        editor.commit();
    }
}

private Bundle restoreFromPreferences() {
    Bundle bundle = null;
    SharedPreferences settings = getSharedPreferences(PREFS, 0);
    String serialized = settings.getString("parcel", null);

    if (serialized != null) {
        Parcel parcel = Parcel.obtain();
        try {
            byte[] data = Base64.decode(serialized, 0);
            parcel.unmarshall(data, 0, data.length);
            parcel.setDataPosition(0);
            bundle = parcel.readBundle();
        } finally {
            parcel.recycle();
        }
    }
    return bundle;
}
asgoth
  • 35,552
  • 12
  • 89
  • 98
  • 6
    This is again going against the recommendation of storing the contents of a Parcel into any form of persistent memory (which the Javadocs warn against). Say you go and update your OS for some reason, then the above code will crash your app in the 'restoreFromPreferences()" method or return some unknown value in the bundle. – Yinzara Jan 29 '13 at 10:28
6

I use SharedPreferences to get around that limitation, it uses the same putXXX() and getXXX() style of storing and retrieving data as the Bundle class does and is relatively simple to implement if you have used a Bundle before.

So in onCreate I have a check like this

if(savedInstanceState != null)
{
    loadGameDataFromSavedInstanceState(savedInstanceState);
}
else
{
    loadGameDataFromSharedPreferences(getPreferences(MODE_PRIVATE));
}

I save my game data to a Bundle in onSaveInstanceState(), and load data from a Bundle in onRestoreInstanceState()

AND

I also save game data to SharedPreferences in onPause(), and load data from SharedPreferences in onResume()

onPause()
{
    // get a SharedPreferences editor for storing game data to
    SharedPreferences.Editor mySharedPreferences = getPreferences(MODE_PRIVATE).edit();

    // call a function to actually store the game data
    saveGameDataToSharedPreferences(mySharedPreferences);

   // make sure you call mySharedPreferences.commit() at the end of your function
}

onResume()
{
    loadGameDataFromSharedPreferences(getPreferences(MODE_PRIVATE));
}

I wouldn't be surprised if some people feel this is an incorrect use of SharedPreferences, but it gets the job done. I have been using this method in all my games (nearly 2 million downloads) for over a year and it works.

snctln
  • 12,175
  • 6
  • 45
  • 42
  • 2
    Sure that works, I was just hoping to avoid having 2 ways of bundling the state, even if they are very similar. – hermo Apr 09 '10 at 04:51
  • This is exactly what I had in mind for saving a persistent state. – Awemo May 13 '12 at 13:06
0

In case you want to store it in persistent storage you can't rely on parcelable nor serializable mechanism. You have to do it by yourself and below is the way how I usually do it:

private static final Gson sGson = new GsonBuilder().create();
private static final String CHARSET = "UTF-8";
// taken from http://www.javacamp.org/javaI/primitiveTypes.html
private static final int BOOLEAN_LEN = 1;
private static final int INTEGER_LEN = 4;
private static final int DOUBLE_LEN = 8;

 public static byte[] serializeBundle(Bundle bundle) {
    try {
        List<SerializedItem> list = new ArrayList<>();
        if (bundle != null) {
            Set<String> keys = bundle.keySet();
            for (String key : keys) {
                Object value = bundle.get(key);
                if (value == null) continue;
                SerializedItem bis = new SerializedItem();
                bis.setClassName(value.getClass().getCanonicalName());
                bis.setKey(key);
                if (value instanceof String)
                    bis.setValue(((String) value).getBytes(CHARSET));
                else if (value instanceof SpannableString) {
                    String str = Html.toHtml((Spanned) value);
                    bis.setValue(str.getBytes(CHARSET));
                } else if (value.getClass().isAssignableFrom(Integer.class)) {
                    ByteBuffer b = ByteBuffer.allocate(INTEGER_LEN);
                    b.putInt((Integer) value);
                    bis.setValue(b.array());
                } else if (value.getClass().isAssignableFrom(Double.class)) {
                    ByteBuffer b = ByteBuffer.allocate(DOUBLE_LEN);
                    b.putDouble((Double) value);
                    bis.setValue(b.array());
                } else if (value.getClass().isAssignableFrom(Boolean.class)) {
                    ByteBuffer b = ByteBuffer.allocate(INTEGER_LEN);
                    boolean v = (boolean) value;
                    b.putInt(v ? 1 : 0);
                    bis.setValue(b.array());
                } else
                    continue; // we do nothing in this case since there is amazing amount of stuff you can put into bundle but if you want something specific you can still add it
//                        throw new IllegalStateException("Unable to serialize class + " + value.getClass().getCanonicalName());

                list.add(bis);
            }
            return sGson.toJson(list).getBytes(CHARSET);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
    throw new IllegalStateException("Unable to serialize " + bundle);
}

public static Bundle deserializeBundle(byte[] toDeserialize) {
    try {
        Bundle bundle = new Bundle();
        if (toDeserialize != null) {
            SerializedItem[] bundleItems = new Gson().fromJson(new String(toDeserialize, CHARSET), SerializedItem[].class);
            for (SerializedItem bis : bundleItems) {
                if (String.class.getCanonicalName().equals(bis.getClassName()))
                    bundle.putString(bis.getKey(), new String(bis.getValue()));
                else if (Integer.class.getCanonicalName().equals(bis.getClassName()))
                    bundle.putInt(bis.getKey(), ByteBuffer.wrap(bis.getValue()).getInt());
                else if (Double.class.getCanonicalName().equals(bis.getClassName()))
                    bundle.putDouble(bis.getKey(), ByteBuffer.wrap(bis.getValue()).getDouble());
                else if (Boolean.class.getCanonicalName().equals(bis.getClassName())) {
                    int v = ByteBuffer.wrap(bis.getValue()).getInt();
                    bundle.putBoolean(bis.getKey(), v == 1);
                } else
                    throw new IllegalStateException("Unable to deserialize class " + bis.getClassName());
            }
        }
        return bundle;
    } catch (Exception e) {
        e.printStackTrace();
    }
    throw new IllegalStateException("Unable to deserialize " + Arrays.toString(toDeserialize));
}

You represent data as byte array which you can easily store to file, send via network or store to sql database using ormLite as follows:

    @DatabaseField(dataType = DataType.BYTE_ARRAY)
    private byte[] mRawBundle;

and SerializedItem:

public class SerializedItem {


private String mClassName;
private String mKey;
private byte[] mValue;

// + getters and setters 
}

PS: the code above is dependent on Gson library (which is pretty common, just to let you know).

vanomart
  • 1,739
  • 1
  • 18
  • 39