How write Java.util.Map into parcel in a smart way?

AndroidBundleParcelable

Android Problem Overview


I have a Generic Map of Strings (Key, Value) and this field is part of a Bean which I need to be parcelable. So, I could use the Parcel#writeMap Method. The API Doc says:

> Please use writeBundle(Bundle) instead. Flattens a Map into the parcel > at the current dataPosition(), growing dataCapacity() if needed. The > Map keys must be String objects. The Map values are written using > writeValue(Object) and must follow the specification there. It is > strongly recommended to use writeBundle(Bundle) instead of this > method, since the Bundle class provides a type-safe API that allows > you to avoid mysterious type errors at the point of marshalling.

So, I could iterate over each Entry in my Map a put it into the Bundle, but I'm still looking for a smarter way doing so. Is there any Method in the Android SDK I'm missing?

At the moment I do it like this:

final Bundle bundle = new Bundle();
final Iterator<Entry<String, String>> iter = links.entrySet().iterator();
while(iter.hasNext())
{
    final Entry<String, String>  entry =iter.next();
    bundle.putString(entry.getKey(), entry.getValue());
}
parcel.writeBundle(bundle);

Android Solutions


Solution 1 - Android

I ended up doing it a little differently. It follows the pattern you would expect for dealing with Parcelables, so it should be familiar.

public void writeToParcel(Parcel out, int flags){
  out.writeInt(map.size());
  for(Map.Entry<String,String> entry : map.entrySet()){
    out.writeString(entry.getKey());
    out.writeString(entry.getValue());
  }
}

private MyParcelable(Parcel in){
  //initialize your map before
  int size = in.readInt();
  for(int i = 0; i < size; i++){
    String key = in.readString();
    String value = in.readString();
    map.put(key,value);
  }
}

In my application, the order of the keys in the map mattered. I was using a LinkedHashMap to preserve the ordering and doing it this way guaranteed that the keys would appear in the same order after being extracted from the Parcel.

Solution 2 - Android

you can try:

bundle.putSerializable(yourSerializableMap);

if your chosen map implements serializable (like HashMap) and then you can use your writeBundle in ease

Solution 3 - Android

If both the key and value of the map extend Parcelable, you can have a pretty nifty Generics solution to this:

Code

// For writing to a Parcel
public <K extends Parcelable,V extends Parcelable> void writeParcelableMap(
        Parcel parcel, int flags, Map<K, V > map)
{
    parcel.writeInt(map.size());
    for(Map.Entry<K, V> e : map.entrySet()){
        parcel.writeParcelable(e.getKey(), flags);
        parcel.writeParcelable(e.getValue(), flags);
    }
}

// For reading from a Parcel
public <K extends Parcelable,V extends Parcelable> Map<K,V> readParcelableMap(
        Parcel parcel, Class<K> kClass, Class<V> vClass)
{
    int size = parcel.readInt();
    Map<K, V> map = new HashMap<K, V>(size);
    for(int i = 0; i < size; i++){
        map.put(kClass.cast(parcel.readParcelable(kClass.getClassLoader())),
                vClass.cast(parcel.readParcelable(vClass.getClassLoader())));
    }
    return map;
}

Usage

// MyClass1 and MyClass2 must extend Parcelable
Map<MyClass1, MyClass2> map;

// Writing to a parcel
writeParcelableMap(parcel, flags, map);

// Reading from a parcel
map = readParcelableMap(parcel, MyClass1.class, MyClass2.class);

Solution 4 - Android

Good question. There aren't any methods in the API that I know of other than putSerializable and writeMap. Serialization is not recommended for performance reasons, and writeMap() is also not recommended for somewhat mysterious reasons as you've already pointed out.

I needed to parcel a HashMap today, so I tried my hand at writing some utility methods for parcelling Map to and from a Bundle in the recommended way:

// Usage:

// read map into a HashMap<String,Foo>
links = readMap(parcel, Foo.class);

// another way that lets you use a different Map implementation
links = new SuperDooperMap<String, Foo>;
readMap(links, parcel, Foo.class);

// write map out
writeMap(links, parcel);

////////////////////////////////////////////////////////////////////
// Parcel methods

/**
 * Reads a Map from a Parcel that was stored using a String array and a Bundle.
 *
 * @param in   the Parcel to retrieve the map from
 * @param type the class used for the value objects in the map, equivalent to V.class before type erasure
 * @return     a map containing the items retrieved from the parcel
 */
public static <V extends Parcelable> Map<String,V> readMap(Parcel in, Class<? extends V> type) {

	Map<String,V> map = new HashMap<String,V>();
	if(in != null) {
		String[] keys = in.createStringArray();
		Bundle bundle = in.readBundle(type.getClassLoader());
		for(String key : keys)
			map.put(key, type.cast(bundle.getParcelable(key)));
	}
	return map;
}


/**
 * Reads into an existing Map from a Parcel that was stored using a String array and a Bundle.
 *
 * @param map  the Map<String,V> that will receive the items from the parcel
 * @param in   the Parcel to retrieve the map from
 * @param type the class used for the value objects in the map, equivalent to V.class before type erasure
 */
public static <V extends Parcelable> void readMap(Map<String,V> map, Parcel in, Class<V> type) {

	if(map != null) {
		map.clear();
		if(in != null) {
			String[] keys = in.createStringArray();
			Bundle bundle = in.readBundle(type.getClassLoader());
			for(String key : keys)
				map.put(key, type.cast(bundle.getParcelable(key)));
		}
	}
}


/**
 * Writes a Map to a Parcel using a String array and a Bundle.
 *
 * @param map the Map<String,V> to store in the parcel
 * @param out the Parcel to store the map in
 */
public static void writeMap(Map<String,? extends Parcelable> map, Parcel out) {
	
	if(map != null && map.size() > 0) {
		/*
		Set<String> keySet = map.keySet();
		Bundle b = new Bundle();
		for(String key : keySet)
			b.putParcelable(key, map.get(key));
		String[] array = keySet.toArray(new String[keySet.size()]);
		out.writeStringArray(array);
		out.writeBundle(b);
		/*/
		// alternative using an entrySet, keeping output data format the same
		// (if you don't need to preserve the data format, you might prefer to just write the key-value pairs directly to the parcel)
		Bundle bundle = new Bundle();
		for(Map.Entry<String, ? extends Parcelable> entry : map.entrySet()) {
			bundle.putParcelable(entry.getKey(), entry.getValue());
		}
			
		final Set<String> keySet = map.keySet();
		final String[] array = keySet.toArray(new String[keySet.size()]);
		out.writeStringArray(array);
		out.writeBundle(bundle);
		/**/
	}
	else {
		//String[] array = Collections.<String>emptySet().toArray(new String[0]);
		// you can use a static instance of String[0] here instead
		out.writeStringArray(new String[0]);
		out.writeBundle(Bundle.EMPTY);
	}
}

Edit: modified writeMap to use an entrySet while preserving the same data format as in my original answer (shown on the other side of the toggle comment). If you don't need or want to preserve read compatibility, it may be simpler to just store the key-value pairs on each iteration, as in @bcorso and @Anthony Naddeo's answers.

Solution 5 - Android

If your map's key is String, you can just use Bundle, as it mentioned in javadocs:

/**
 * Please use {@link #writeBundle} instead.  Flattens a Map into the parcel
 * at the current dataPosition(),
 * growing dataCapacity() if needed.  The Map keys must be String objects.
 * The Map values are written using {@link #writeValue} and must follow
 * the specification there.
 *
 * <p>It is strongly recommended to use {@link #writeBundle} instead of
 * this method, since the Bundle class provides a type-safe API that
 * allows you to avoid mysterious type errors at the point of marshalling.
 */
public final void writeMap(Map val) {
    writeMapInternal((Map<String, Object>) val);
}

So I wrote the following code:

private void writeMapAsBundle(Parcel dest, Map<String, Serializable> map) {
    Bundle bundle = new Bundle();
    for (Map.Entry<String, Serializable> entry : map.entrySet()) {
        bundle.putSerializable(entry.getKey(), entry.getValue());
    }
    dest.writeBundle(bundle);
}

private void readMapFromBundle(Parcel in, Map<String, Serializable> map, ClassLoader keyClassLoader) {
    Bundle bundle = in.readBundle(keyClassLoader);
    for (String key : bundle.keySet()) {
        map.put(key, bundle.getSerializable(key));
    }
}

Accordingly, you can use Parcelable instead of Serializable

Solution 6 - Android

Here's mine somewhat simple but working so far for me implementation in Kotlin. It can be modified easily if it doesn't satisfy one needs

But don't forget that K,V must be Parcelable if different than the usual String, Int,... etc

Write

parcel.writeMap(map)

Read

parcel.readMap(map)

The read overlaod

fun<K,V> Parcel.readMap(map: MutableMap<K,V>) : MutableMap<K,V>{

    val tempMap = LinkedHashMap<Any?,Any?>()
    readMap(tempMap, map.javaClass.classLoader)

    tempMap.forEach {
        map[it.key as K] = it.value as V
    }
    /* It populates and returns the map as well
       (useful for constructor parameters inits)*/
    return map
}

Solution 7 - Android

All the solutions mentioned here are valid but no one is universal enough. Often you have maps containing Strings, Integers, Floats etc. values and/or keys. In such a case you can't use <... extends Parcelable> and I don't want to write custom methods for any other key/value combinations. For that case you can use this code:

@FunctionalInterface
public interface ParcelWriter<T> {
    void writeToParcel(@NonNull final T value,
                       @NonNull final Parcel parcel, final int flags);
}

@FunctionalInterface
public interface ParcelReader<T> {
    T readFromParcel(@NonNull final Parcel parcel);
}

public static <K, V> void writeParcelableMap(
        @NonNull final Map<K, V> map,
        @NonNull final Parcel parcel,
        final int flags,
        @NonNull final ParcelWriter<Map.Entry<K, V>> parcelWriter) {
    parcel.writeInt(map.size());

    for (final Map.Entry<K, V> e : map.entrySet()) {
        parcelWriter.writeToParcel(e, parcel, flags);
    }
}

public static <K, V> Map<K, V> readParcelableMap(
        @NonNull final Parcel parcel,
        @NonNull final ParcelReader<Map.Entry<K, V>> parcelReader) {
    int size = parcel.readInt();
    final Map<K, V> map = new HashMap<>(size);

    for (int i = 0; i < size; i++) {
        final Map.Entry<K, V> value = parcelReader.readFromParcel(parcel);
        map.put(value.getKey(), value.getValue());
    }
    return map;
}

It's more verbose but universal. Here is the write usage:

writeParcelableMap(map, dest, flags, (mapEntry, parcel, __) -> {
        parcel.write...; //key from mapEntry
        parcel.write...; //value from mapEntry
    });

and read:

map = readParcelableMap(in, parcel ->
    new AbstractMap.SimpleEntry<>(parcel.read... /*key*/, parcel.read... /*value*/)
);

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionKitesurferView Question on Stackoverflow
Solution 1 - AndroidAnthony NaddeoView Answer on Stackoverflow
Solution 2 - AndroidSTT LCUView Answer on Stackoverflow
Solution 3 - AndroidbcorsoView Answer on Stackoverflow
Solution 4 - AndroidLorne LaliberteView Answer on Stackoverflow
Solution 5 - AndroidrepitchView Answer on Stackoverflow
Solution 6 - AndroidJocky DoeView Answer on Stackoverflow
Solution 7 - Androidbio007View Answer on Stackoverflow