I'm attempting to pass a serialized LinkedHashMap between activities, and getting a confusing result/error when I deserialize the object.
I serialize the object as follows:
Bundle exDetails = new Bundle();
LinkedHashMap<String, Exercise> exMap = new LinkedHashMap<String,
Exercise (workout.getExercises());
exDetails.putString(WORKOUTNAME, workout.getWorkoutName());
exDetails.putSerializable(workout.getWorkoutName(), exMap);
iComplete.putExtra("wName", exDetails);
startActivity(iComplete);
That seems to work fine, the problem shows up in the next activity:
Bundle exDetails = getIntent().getBundleExtra("wName");
workoutName = exDetails.getString(WORKOUTNAME);
Serializable eData = exDetails.getSerializable(workoutName);
ex = new LinkedHashMap<String, Exercise>();
ex = (LinkedHashMap<String, Exercise>) eData;
At this point, the deserialized object (eData) contains a HashMap object (not LinkedHashMap), and it gives me
java.lang.ClassCastException:java.util.HashMap cannot be
cast to java.util.LinkedHashMap
on that last line. I've verified with the debugger that the bundle (in the second activity) contains a HashMap, instead of a LinkedHashMap (as I'm assuming it should). I should also mention that I need to maintain the order in which entries are added to the Map, hence the usage of LinkedHashMap. The entries eventually get printed, and order is very important for the output.
Questions:
Am I doing anything wrong in particular, or is this problem due to bugs with LinkedHashMap's serialization? I've noticed a few similar threads, that seem to speak of this being an ongoing problem with several of the Map implementations. They didn't answer my problem directly though.
If the latter, is there a workaround that isn't too advanced (I'm not far beyond beginner level, but I'm willing to try most things), or do I need to just bite the bullet and work something other than LinkedHashMap?
P.s. I tried to include everything relevant, but I can add more code if I left out anything important.
I went for a different approach: serialize any (type of) Map into 2 ArrayLists: one containing the keys and the other one containing the values. This way, the order of the map entries (important in a LinkedHashMap) is kept.
Each key/value implements Serializable. If you know for sure you need just Strings or just one particular type of Map, then it should be really easy to convert the following generic code into the scenario needed which also simplifies the complexity.
Map -> 2 ArrayLists:
public static <K extends Serializable, V extends Serializable> Pair<ArrayList<K>, ArrayList<V>> convertMapToArrays(#NonNull Map<K, V> map) {
final Set<Map.Entry<K, V>> entries = map.entrySet();
final int size = entries.size();
final ArrayList<K> keys = new ArrayList<>(size);
final ArrayList<V> values = new ArrayList<>(size);
for (Map.Entry<K, V> entry : entries) {
keys.add(entry.getKey());
values.add(entry.getValue());
}
return new Pair<>(keys, values);
}
2 ArrayLists -> Map of a specific type
public static <K extends Serializable, V extends Serializable> Map<K, V> convertArraysToMap(#NonNull ArrayList<K> keys, #NonNull ArrayList<V> values, #NonNull Class<? extends Map<K, V>> mapClass) {
if (keys.size() != values.size()) {
throw new RuntimeException("keys and values must have the same number of elements");
}
final int size = keys.size();
Map<K, V> map;
try {
final Constructor<? extends Map<K, V>> constructor = mapClass.getConstructor(Integer.TYPE);
map = constructor.newInstance(size);
} catch (Exception nse) {
throw new RuntimeException("Map constructor that accepts the initial capacity not found.");
}
for (int i = 0; i < size; i++) {
final K key = keys.get(i);
final V value = values.get(i);
map.put(key, value);
}
return map;
}
Helpers for Android's Bundle:
public static <K extends Serializable, V extends Serializable> void saveMapToBundleAsArrays(#NonNull Map<K, V> map, #NonNull String key, #NonNull Bundle bundle) {
final Pair<ArrayList<K>, ArrayList<V>> mapToArrays = convertMapToArrays(map);
final String keyForKeys = key + "_keys";
final String keyForValues = key + "_values";
bundle.putSerializable(keyForKeys, mapToArrays.first);
bundle.putSerializable(keyForValues, mapToArrays.second);
}
public static Map<Serializable, Serializable> loadMapFromBundle(#NonNull Bundle bundle, #NonNull String key, #NonNull Class<? extends Map<Serializable, Serializable>> mapClass) {
final String keyForKeys = key + "_keys";
final String keyForValues = key + "_values";
final ArrayList<Serializable> keys = (ArrayList<Serializable>) bundle.getSerializable(keyForKeys);
final ArrayList<Serializable> values = (ArrayList<Serializable>) bundle.getSerializable(keyForValues);
return convertArraysToMap(keys, values, mapClass);
}
Usage:
saveMapToBundleAsArrays(mModelEvolution, KEY_MODEL_DATA, bundle);
Class<LinkedHashMap<Serializable, Serializable>> linkedHashMapClazz =
(Class<LinkedHashMap<Serializable, Serializable>>) new LinkedHashMap<String, String>().getClass();
mModelEvolution = (LinkedHashMap) ObjectUtils.loadMapFromBundle(bundle, KEY_MODEL_DATA, linkedHashMapClazz);
Related
Seems like Gson.toJson(Object object) generates JSON code with randomly spread fields of the object. Is there way to fix fields order somehow?
public class Foo {
public String bar;
public String baz;
public Foo( String bar, String baz ) {
this.bar = bar;
this.baz = baz;
}
}
Gson gson = new Gson();
String jsonRequest = gson.toJson(new Foo("bar","baz"));
The string jsonRequest can be:
{ "bar":"bar", "baz":"baz" } (correct)
{ "baz":"baz", "bar":"bar" } (wrong sequence)
You'd need to create a custom JSON serializer.
E.g.
public class FooJsonSerializer implements JsonSerializer<Foo> {
#Override
public JsonElement serialize(Foo foo, Type type, JsonSerializationContext context) {
JsonObject object = new JsonObject();
object.add("bar", context.serialize(foo.getBar());
object.add("baz", context.serialize(foo.getBaz());
// ...
return object;
}
}
and use it as follows:
Gson gson = new GsonBuilder().registerTypeAdapter(Foo.class, new FooJsonSerializer()).create();
String json = gson.toJson(foo);
// ...
This maintains the order as you've specified in the serializer.
See also:
Gson User Guide - Custom serializers and deserializers
If GSON doesn't support definition of field order, there are other libraries that do. Jackson allows definining this with #JsonPropertyOrder, for example. Having to specify one's own custom serializer seems like awful lot of work to me.
And yes, I agree in that as per JSON specification, application should not expect specific ordering of fields.
Actually Gson.toJson(Object object) doesn't generate fields in random order. The order of resulted json depends on literal sequence of the fields' names.
I had the same problem and it was solved by literal order of properties' names in the class.
The example in the question will always return the following jsonRequest:
{ "bar":"bar", "baz":"baz" }
In order to have a specific order you should modify fields' names, ex: if you want baz to be first in order then comes bar:
public class Foo {
public String f1_baz;
public String f2_bar;
public Foo ( String f1_baz, String f2_bar ) {
this.f1_baz = f1_baz;
this.f2_bar = f2_bar;
}
}
jsonRequest will be { "f1_baz ":"baz", "f2_bar":"bar" }
Here's my solution for looping over json text files in a given directory and writing over the top of them with sorted versions:
private void standardizeFormat(File dir) throws IOException {
File[] directoryListing = dir.listFiles();
if (directoryListing != null) {
for (File child : directoryListing) {
String path = child.getPath();
JsonReader jsonReader = new JsonReader(new FileReader(path));
Gson gson = new GsonBuilder().setPrettyPrinting().registerTypeAdapter(LinkedTreeMap.class, new SortedJsonSerializer()).create();
Object data = gson.fromJson(jsonReader, Object.class);
JsonWriter jsonWriter = new JsonWriter(new FileWriter(path));
jsonWriter.setIndent(" ");
gson.toJson(data, Object.class, jsonWriter);
jsonWriter.close();
}
}
}
private class SortedJsonSerializer implements JsonSerializer<LinkedTreeMap> {
#Override
public JsonElement serialize(LinkedTreeMap foo, Type type, JsonSerializationContext context) {
JsonObject object = new JsonObject();
TreeSet sorted = Sets.newTreeSet(foo.keySet());
for (Object key : sorted) {
object.add((String) key, context.serialize(foo.get(key)));
}
return object;
}
}
It's pretty hacky because it depends on the fact that Gson uses LinkedTreeMap when the Type is simply Object. This is an implementation details that is probably not guaranteed. Anyway, it's good enough for my short-lived purposes...
Currently working on an app that takes results from a search, parses the JSON object returned, and then adds the resulting pieces into a few ArrayLists within a class created called VenueList.
Here is the method that receives the results from the service and parses the JSON:
private static List<String> getResultsFromJson(String json) {
ArrayList<String> resultList = new ArrayList<String>();
try {
JSONObject resultsWrapper = (JSONObject) new JSONTokener(json).nextValue();
JSONArray results = resultsWrapper.getJSONArray("results");
for (int i = 0; i < results.length(); i++) {
JSONObject result = results.getJSONObject(i);
resultList.add(result.getString("text"));
}
}
catch (JSONException e) {
Log.e(TAG, "Failed to parse JSON.", e);
}
return resultList;
}
What results of this becomes a List variable call mResults (to clarify: mResults = getResultsFromJson(restResult);. That is then used, among other places, in the following loop that puts the results into an ArrayAdapter that is used for displaying them in a ListFragment:
for (String result : mResults) {
VenueList.addVenue(result, "HELLO WORLD");
adapter.add(result);
}
I also add the result to a class called VenueList that manages the results and makes them accessible for multiple views. It essentially just holds multiple ArrayLists that hold different types of details for each venue returned in the search. The method I use to add a venue to VenueList is below (and you can see it used in the for loop above):
public static void addVenue(String name, String geo) {
venueNames.add(name);
venueGeos.add(geo);
}
I want the addVenue method to be able to take multiple arguments and update the VenueList class. Yet, when I call the addVenue method in the for loop, I can only pass it String result (from the parameters of the loop) and can't figure out how to pass it a second argument (which should also come from the JSON parsed by getResultsFromJson) so I've used "HELLO WORLD" as a placeholder for now.
I realize getResultsFromJson only has one list returned. I need to be able to take multiple elements from the JSON object that I parse, and then add them to VenueList in the right order.
So my questions are:
1) Given the getResultsFromJson method and the for loop, how can I use the addVenue() method as designed? How do I parse multiple elements from the JSON, and then add them to the VenueList at the same time? I plan on adding more arguments to it later on, but I assume if I can make it work with two, I can make it work with four or five.
2) If that's not possible, how should the getResultsFromJson, the for loop, and the addVenue method be redesigned to work properly together?
Please let me know if you need more detail or code - happy to provide. Thank you!
EDIT - Full VenueList class:
public class VenueList {
private static ArrayList<String> venueNames;
private static ArrayList<String> venueGeos;
public VenueList() {
venueNames = new ArrayList<String>();
venueGeos = new ArrayList<String>();
}
public static void addVenue(String name, String geo) {
venueNames.add(name);
venueGeos.add(geo);
}
public static String getVenueName(int position) {
return venueNames.get(position);
}
public static String getVenueGeo(int position) {
return venueGeos.get(position);
}
public static void clearList() {
venueNames.clear();
venueGeos.clear();
}
}
Clarification: I will have additional ArrayLists for each element of data that I want to store about a venue (phone number, address, etc etc)
1) I don't think methods getResultsFromJson(String json) and addVenue(String name, String geo) fit your needs.
2) I would consider rewriting method getResultsFromJson(String json) to something like this:
private static SortedMap<Integer, List<String>> getResultsFromJson(String json) {
Map<Integer, String> resultMap = new TreeMap<Integer, String>();
//...
return resultMap;
}
where the number of keys of your map should be equal to the number of objects you're extracting info, and each one of them will properly have their own list of items just in the right order you extract them.
With this approach you can certainly change your logic to something like this:
// grab your retuned map and get an entrySet, the just iterate trough it
SortedMap<Integer, String> result = returnedMap.entrySet();
for (Entry<Integer, String> result : entrySet) {
Integer key = result.getKey(); // use it if you need it
List<String> yourDesiredItems = result.getValue(); // explicitly shown to know how to get it
VenueList.addVenue(yourDesiredItems);
}
public static void addVenue(List<String> yourDesiredItems) {
// refactor here to iterate the items trough the list and save properly
//....
}
EDIT -- as you wish to avoid the go-between map i'm assuming you need nothing to return from the method
First i'm providing you with a solution to your requirements, then i'll provide you with some tips cause i see some things that could smplify your design.
To save VenueList things directly from getResultsFromJSON do something like this:
private static void getResultsFromJson(String json) {
try {
JSONObject resultsWrapper = (JSONObject) new JSONTokener(json).nextValue();
JSONArray results = resultsWrapper.getJSONArray("results");
for (int i = 0; i < results.length(); i++) {
JSONObject result = results.getJSONObject(i);
//FOR EXAMPLE HERE IS WHERE YOU NEED TO EXTRACT INFO
String name = result.getString("name");
String geo = result.getString("geo");
// and then...
VenueList.addVenue(name, geo, ..., etc);
}
} catch (JSONException e) {
Log.e(TAG, "Failed to parse JSON.", e);
}
}
This implies that your addVenue method should know receive all params needed; as you can see this is just a way (that you can consider a workaround to your needs), however as i don't know all requirements that lead you to code this model, i will point to a few things you might consider:
1. If there's a reason for VenueList class to use everything static, consider doing this:
static{
venueNames = new ArrayList<String>();
venueGeos = new ArrayList<String>();
//....
}
private VenueList(){
}
This way you won't need to get an instance every time and also will avoid null pointer exceptions when doing VenueList.addVenue(...) without previous instantiation.
2. Instead of having an ArrayList for every characteristic in VenueList class consider defining a model object for a Venue like this:
public class Venue{
String name;
String geo;
//... etc
public Venue(){
}
// ... getters & setters
}
then if you need that VenueList class you will just have a list o Venue objects (List<Venue>), this means that instead of calling the method addVenue, you will first create a brand new instance of Venue class and will call the setter method of each characteristic, as an example of the refactored for loop from the workaround i provided you you'd be using something like this:
List<Venue> myListOfVenues = new ArrayList<Venue>();
for (int i = 0; i < results.length(); i++) {
JSONObject result = results.getJSONObject(i);
// THIS WOULD REMAIN THE SAME TO EXTRACT INFO
String name = result.getString("name");
String geo = result.getString("geo");
// and then instead of calling VenueList.addVenue(name, geo, ..., etc)...
Venue v = new Venue();
v.setName(name);
v.setGeo(geo);
// ...etc
myListOfVenues.add(v);
}
// Once you're done, set that List to VenueList class
VenueList.setVenueList(myListOfVenues);
So VenueList class would now have a single property List<Venue> venueList; and would suffer minor tweeks on methods getVenueName, etc... and everything would be more readable... i hope this helps you to get another approach to solve your problem, if i still don't make my point let me know and i'll try to help you out...
I use ormlite and I have a db with a field:
public static final String NAME = "name";
#DatabaseField (canBeNull = false, dataType = DataType.SERIALIZABLE, columnName = NAME)
private String[] name = new String[2];
And I would like to get all elements that name[0] and name[1] are "car". I try to add a where clausule like:
NAMEDB nameDB = null;
Dao<NAMEDB, Integer> daoName = this.getHelper().getDao(NAMEDB.class);
QueryBuilder<NAMEDB, Integer> queryName = daoName.queryBuilder();
Where<NAMEDB, Integer> where = queryName.where();
where.in(nameDb.NAME, "car");
But it doesn't work because it's an array string.
I have other fields:
public static final String MARK = "mark";
#DatabaseField (canBeNull = false, foreign = true, index = true, columnName = MARK)
private String mark = null;
And I can do this:
whereArticulo.in(nameDB.MARK, "aaa");
How can I solve my problem? Thanks.
It seems to me that a third option to store a string array (String[] someStringArray[]) in the database using Ormlite would be to define a data persister class that converts the string array to a single delimited string upon storage into the database and back again to a string array after taking it out of the database.
E.g., persister class would convert ["John Doe", "Joe Smith"] to "John Doe | Joe Smith" for database storage (using whatever delimiter character makes sense for your data) and converts back the other way when taking the data out of the database.
Any thoughts on this approach versus using Serializable or a foreign collection? Anyone tried this?
I just wrote my first persister class and it was pretty easy. I haven't been able to identify through web search or StackOverflow search that anyone has tried this.
Thanks.
As ronbo4610 suggested, it is a good idea to use a custom data persister in this case, to store the array as a string in the database separated by some kind of delimiter. You can then search the string in your WHERE clause just as you would any other string. (For example, using the LIKE operator)
I have implemented such a data persister. In order to use it, you must add the following annotation above your String[] object in your persisted class:
#DatabaseField(persisterClass = ArrayPersister.class)
In addition, you must create a new class called "ArrayPersister" with the following code:
import com.j256.ormlite.field.FieldType;
import com.j256.ormlite.field.SqlType;
import com.j256.ormlite.field.types.StringType;
import org.apache.commons.lang3.StringUtils;
public class ArrayPersister extends StringType {
private static final String delimiter = ",";
private static final ArrayPersister singleTon = new ArrayPersister();
private ArrayPersister() {
super(SqlType.STRING, new Class<?>[]{ String[].class });
}
public static ArrayPersister getSingleton() {
return singleTon;
}
#Override
public Object javaToSqlArg(FieldType fieldType, Object javaObject) {
String[] array = (String[]) javaObject;
if (array == null) {
return null;
}
else {
return StringUtils.join(array, delimiter);
}
}
#Override
public Object sqlArgToJava(FieldType fieldType, Object sqlArg, int columnPos) {
String string = (String)sqlArg;
if (string == null) {
return null;
}
else {
return string.split(delimiter);
}
}
}
Unfortunately ORMLite does not support querying fields that are the type SERIALIZABLE. It is storing the array as a serialized byte[] so you cannot query against the values with an IN query like:
where.in(nameDb.NAME, "car");
ORMLite does support foreign collections but you have to set it up yourself with another class holding the names. See the documentation with sample code:
http://ormlite.com/docs/foreign-collection
I have a problem on arraylist and hashmap
As according to my requirement, I am storing the data into HashMap and after that I have created a List as List>.
HashMap<String,String> hashmap;
static List<HashMap<String,String>> hashmap_string;
And while retrieving the value from database and putting it on HashMap and ArrayList like:
contract_number=c.getString(c1);
Log.i("c1.getString,contract_number", contract_number);
String service_level=c.getString(c2);
hashmap=new HashMap<String, String>();
hashmap.put(contract_number, service_level);
hashmap_string.add(hashmap);
And now I want to retrieve the value as String,String
And when I am applying the code as:
for(int i=0;i<hashmap_string.size();i++)
{
Log.i("arraylist", ""+hashmap_string.get(i));
}
I am getting a single string value in the formet as
{Contract,ServiveValue}
but I want to split this into 2 string values...Also these values are redundant and if am using hashMap then it will not showing me the redundant value.
Please help me on this..
It seems you are missing something. When you execute hashmap_string.get(i), you will get the <HashMap<String,String>. So, This is the right value from code.
What you can do is :
HashMap<String, String> hashMap2 = hashmap_string.get(i);
String value = hashMap2.get("your_key");
Other way, you already have two splited string values. you can get that by using keySet() and values() methods over hashMap2 Object.
HashMap (and Maps in general) are used for multiple one-to-one mappings of keys and values. Are you sure you need that? Looking at your code it appears you're using the map as a "Pair" class. I would skip the list, and put everything in the same map, and then iterate over the pairs in the map:
// using tree map to have entries sorted on the key,
// rather than the key's hash value.
Map<String, String> data = new TreeMap<String, String>();
data.put("c1", "s1");
data.put("c2", "s2");
for (Map.Entry<String, String> entry : data.entrySet()) {
String contract = entry.getKey();
String level = entry.getValue();
Log.i("data", contract + " : " + level");
}
would output (assuming TreeSet):
c1 : s1
c2 : s2
Alternatively, create e.g. a ContractServiceLevel class that holds two strings (the contract number and the service level), and put instances of that class in your list.
EDIT:
public final class ContractServiceLevel {
public final String number;
public final String serviceLevel;
public ContractServiceLevel(String c, String s) {
number = c;
serviceLevel = s;
}
}
List<ContractServiceLevel> contracts = new ArrayList<ContractServiceLevel>();
contracts.add(new ContractServiceLevel("c1", "s1.1"));
contracts.add(new ContractServiceLevel("c1", "s1.2"));
contracts.add(new ContractServiceLevel("c2", "s2.1"));
for (ContractServiceLevel contract : contracts) {
Log.i("data", contract.number + ":" + contract.servicveLevel);
}
would output:
c1 : s1.1
c1 : s1.2
c2 : s2.1
String value = hashmap.get("contract");
u will be getting the value as ServiveValue
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);
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.
you can try:
bundle.putSerializable(yourSerializableMap);
if your chosen map implements serializable (like HashMap) and then you can use your writeBundle in ease
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);
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.
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
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
}
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*/)
);