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

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*/)
);

Related

add hashmap to another

i have an static hashmap :
private static HashMap<String, byte[]> mDrawables = new HashMap<>();
by thread I download an image as a byte[] and i want add this new hashmap to static hashmap.
protected void onResult(String srv, HashMap<String, byte[]> drawables) {
super.onResult(srv, drawables);
mDrawables.putAll(drawables);
}
but every time that invoked putAll ,all info on mDrawables is cleared .
how could i add new map key, value to static once??
Well, accordint to JavaDoc:
/**
* Copies all of the mappings from the specified map to this map.
* These mappings will replace any mappings that this map had for
* any of the keys currently in the specified map.
*
* #param m mappings to be stored in this map
* #throws NullPointerException if the specified map is null
*/
So, the same keys will be replaced. You can use Map#put() in a cycle and check it by yourself like this:
for (Map.Entry<String, byte[]> entry : drawables.entrySet()) {
if (mDrawables.containsKey(entry.getKey())) {
// duplicate key is found
} else {
mDrawables.put(entry.getKey(), entry.getValue());
}
}

Naming convention with Firebase serialization/deserialization?

I wonder to know how Firebase serialize/deserialize POJO object to/from json, does it use Jackson or Gson or any similar library else.
I have trouble about naming convention with Firebase. My model some like this:
class Data {
private String someFieldName;
private String anotherFieldName;
public Data() {}
public void setSomeFieldName(String) {...}
public String getSomeFieldName(String) {...}
public void setAnotherFieldName(String) {...}
public String getAnotherFieldName() {...}
}
And the expected result in Firebase should be:
{
"some_field_name" : "...",
"another_field_name" : "..."
}
with Gson I can use FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES for my purpose, as in Gson doc:
Here's a few examples of the form "Java Field Name" ---> "JSON Field Name":
someFieldName ---> some_field_name
_someFieldName ---> _some_field_name
aStringField ---> a_string_field
aURL ---> a_u_r_l
How can I convert my POJO object to "Firebase value" with specific naming convention and vice versa, or there are any way to customize the serialize/deserialize process?
Thanks!
When reading the data back from the Firebase database you can use the #PropertyName annotation to mark a field to be renamed when being serialized/deserialized, like so:
#IgnoreExtraProperties
class Data {
#PropertyName("some_field_name")
public String someFieldName
#PropertyName("another_field_name")
private String anotherFieldName;
public Data() {}
}
Make sure that your field is public and not private or else the annotation will not work (I also believe that Firebase uses Jackson to handle the object mapping under the hood, but don't think you can actually customize HOW it uses it).
Personally I prefer keeping explicit control over the serialization/deserialization process, and not relying on specific framework and/or annotations.
Your Data class can be simply modified like this :
class Data {
private String someFieldName;
private String anotherFieldName;
public Data() {}
public Data(Map<String, Object> map) {
someFieldName = (String) map.get("some_field_name") ;
anotherFieldName = (String) map.get("another_field_name") ;
}
public Map<String, Object> toMap() {
Map<String, Object> map = new HashMap<>();
map.put("some_field_name", someFieldName);
map.put("another_field_name", anotherFieldName);
return map ;
}
}
For writing value to firebase, simply do :
dbref.setValue(data.toMap());
For reading :
Map<String, Object> map = (Map<String, Object>) dataSnapshot.getValue();
data = new Data(map);
They are some advantages with this solution :
No assumption is made on underlying json framework
No need to use annotations
You can even further decouple you Object model from your Json model by externalizing the methods toMap() and constructor to a DataMapper (snippet hereunder)
public static Data fromMap(Map<String, Object> map) {
String someFieldName = (String) map.get("some_field_name") ;
String anotherFieldName = (String) map.get("another_field_name") ;
return new Data(someFieldName, anotherFieldName);
}
public static Map<String, Object> toMap(Data data) {
Map<String, Object> map = new HashMap<>();
map.put("some_field_name", data.getSomeFieldName());
map.put("another_field_name", data.getAnotherFieldName());
return map ;
}

Passing Hashmap through android intents [duplicate]

This question already has answers here:
Android - How to pass HashMap<String,String> between activities?
(5 answers)
Closed 6 years ago.
I have a HashMap(String,HashMap(String,Object)) in one of my activity. How do I send this HashMap to another activity via intents
How to add this HashMap to intent extras?
Sending HashMap
HashMap<String, Object> hashmapobject =new HashMap<>();
HashMap<String, Object> newhashMap=new HashMap<>();
hashmapobject.put("key",newhashMap);
Intent intent = new Intent(SenderActivity.this, NextActivity.class);
intent.putExtra("hashMapKey", hashmapobject);
startActivity(intent);
Receiving HashMap
Intent intent = getIntent();
HashMap<String, Object> hashMapObject = (HashMap<String, Object>) intent.getSerializableExtra("hashMapKey");
HashMap<String, Object> newhashMap=(HashMap<String, Object>)hashMapObject.get("key");
There could be multiple approaches to your problem and each one depends on what datatype you are storing in map. Easiest approach is to use JSONObject from json.org pass it as String and while receiving convert it back to JSON. JSONObject uses a LinkedHashMap inside thus you will be able to use the features of HashMap.
JSONObject obj=new JSONObject();
obj.put("key1", "value1");
obj.put("key2", "value2");
obj.put("key3", "value3");
.
.
.
obj.put("keyN", "valueN");
intent.putExtra("map", obj.toString());
While receiving
JSONObject obj=new JSONObject(getIntent().getStringExtra("map"));
For complex datatypes try considering either JSON Libraries like GSON or use Parcelable interface.
Hi you can use Parcelable :
Write class like this :
public class Person implements Parcelable {
String name;
int age;
Date brithDate;
public Person(String name, int age, Date brithDate) {
this.name = name;
this.age = age;
this.brithDate = brithDate;
}
#Override
public int describeContents() {
return 0;
}
#Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(this.name);
dest.writeInt(this.age);
dest.writeLong(brithDate != null ? brithDate.getTime() : -1);
}
protected Person(Parcel in) {
this.name = in.readString();
this.age = in.readInt();
long tmpbrithDate = in.readLong();
this.brithDate = tmpbrithDate == -1 ? null : new Date(tmpbrithDate);
}
public static final Parcelable.Creator<Person> CREATOR = new Parcelable.Creator<Person>() {
public Persona createFromParcel(Parcel source) {
return new Person(source);
}
public Person[] newArray(int size) {
return new Person[size];
}
};
Put extra :
intent.putExtra("person", new Person("Ahmad",34,date));
Get :
Bundle data = getIntent().getExtras();
Person person= (Person) data.getParcelable("person");
OR you can copy class this site and convert to Parcelable class :
http://www.parcelabler.com/
OR you can use this library hrisey
https://github.com/mg6maciej/hrisey/wiki/Parcelable#details
Or you can use Android Studio have plugins for this:
Android Parcelable code generator (Apache License 2.0)
Auto Parcel (The MIT License)
SerializableParcelable Generator (The MIT License)
Parcelable Code Generator (for Kotlin) (Apache License 2.0)

Can Nulls be passed to parcel?

Is it possible to write null to Parcel when parcelling an object, and get null back again when unparcelling it again?
Let's assume we have the following code:
public class Test implements Parcelable {
private String string = null;
public Test(Parcel dest, int flags) {
source.writeString(string);
}
}
Will I get a NullPointerException when reading this value back from the parcel using Parcel.readString()?
Or will I get a null value out?
Yes, you can pass a null to the Parcel.writeString(String) method.
When you read it out again with Parcel.readString(), you will get a null value out.
For example, assume you have a class with the following fields in it:
public class Test implements Parcelable {
public final int id;
private final String name;
public final String description;
...
You create the Parcelable implementation like this (using Android Studio autoparcelable tool):
#Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(id);
dest.writeString(null); // NOTE the null value here
dest.writeString(description);
}
protected Test(Parcel in) {
id = in.readInt();
name = in.readString();
description = in.readString();
}
When running this code, and passing a Test object as a Parcelable extra in an Intent, 2 points become apparent:
the code runs perfectly without any NullPointerException
the deserialised Test object has a value name == null
You can see similar info in the comments to this related question:
Parcel, ClassLoader and Reading Null values
If you want to write other data types such as Integer, Double, Boolean with possible null values to a parcel, you can use Parcel.writeSerializable().
When reading these values back from parcel, you have to cast the value returned by Parcel.readSerializable() to the correct data type.
Double myDouble = null;
dest.writeSerializable(myDouble); // Write
Double readValue = (Double) dest.readSerializable(); // Read
In my case
(Kotlin)
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeInt(if (PapperId == null) -1 else PapperId)
parcel.writeString( if (Nome == null) "" else Nome)
}

Android LinkedHashMap deserializing as HashMap, causes CCE error

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);

Categories

Resources