I am using Parcelable to communicate between fragments. Everything is working good, but sometimes when Android kills app process to free up the memory and user returns to app, the Parcelable gets huge amount of items from arrays like couple millions items instead of 2-3 and of course it throws OOM.
I suppose something's wrong with CREATOR, but I can't handle it.
The code for parcelable is composed from super class and child class and my interested item class:
base class:
public abstract class BaseRs implements Parcelable {
private String token;
private String msgAlert;
private String simpleMsg;
private AdsObj adsObj;
private Map<String, String> mapSettings;
//getters & setters
public BaseRs() {
}
protected BaseRs(Parcel in) {
token = in.readString();
msgAlert = in.readString();
simpleMsg = in.readString();
adsObj = in.readParcelable(AdsObj.class.getClassLoader());
mapSettings = MapParcelable.readParcelable(in);
}
#Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(token);
dest.writeString(msgAlert);
dest.writeString(simpleMsg);
dest.writeParcelable(adsObj, flags);
MapParcelable.writeToParcel(dest, mapSettings);
}
}
child class
public class GetSalesItemsRs extends BaseRs {
private SaleCoinItem[] coinPacksArray;
//getters+ setters
protected GetSalesItemsRs(Parcel in) {
super(in);
coinPacksArray = in.createTypedArray(SaleCoinItem.CREATOR);
}
#Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeTypedArray(coinPacksArray, flags);
}
#Override
public int describeContents() {
return 0;
}
public static final Creator<GetSalesItemsRs> CREATOR = new Creator<GetSalesItemsRs>() {
#Override
public GetSalesItemsRs createFromParcel(Parcel in) {
return new GetSalesItemsRs(in);
}
#Override
public GetSalesItemsRs[] newArray(int size) {
return new GetSalesItemsRs[size];
}
};
}
and my interested object
public class SaleCoinItem implements Parcelable {
private int amount;
private double price;
private int sortOrder;
//getters & setters
protected SaleCoinItem(Parcel in) {
amount = in.readInt();
price = in.readDouble();
sortOrder = in.readInt();
}
#Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(amount);
dest.writeDouble(price);
dest.writeInt(sortOrder);
}
#Override
public int describeContents() {
return 0;
}
public static final Creator<SaleCoinItem> CREATOR = new Creator<SaleCoinItem>() {
#Override
public SaleCoinItem createFromParcel(Parcel in) {
return new SaleCoinItem(in);
}
#Override
public SaleCoinItem[] newArray(int size) {
return new SaleCoinItem[size];
}
};
}
The functiona to parcel Map - maybe here is the error ;)
public class MapParcelable {
public static void writeToParcel(Parcel out, Map<String, String> map) {
if (map != null) {
out.writeInt(map.size());
for (Map.Entry<String, String> entry : map.entrySet()) {
out.writeString(entry.getKey());
out.writeString(entry.getValue());
}
}
}
public static Map<String, String> readParcelable(Parcel parcel) {
Map<String, String> map = new HashMap<String, String>();
int size = parcel.readInt();
for (int i = 0; i < size; i++) {
String key = parcel.readString();
String value = parcel.readString();
map.put(key, value);
}
return map;
}
}
I get parcelable obj inside the onCreateView method :
GetSalesItemsRs mGetSalesItemsRs = getArguments().getParcelable(KEY_PARCEL);
I think the problem is that if the Map is null you're not writing the size field to the Parcel, but when you read it you always read the size field, and it might end up reading some random value. So or you change the code to
if (map != null) {
out.writeInt(map.size());
for (Map.Entry<String, String> entry : map.entrySet()) {
out.writeString(entry.getKey());
out.writeString(entry.getValue());
}
}else{
out.writeInt(0);
}
and you will never get a null value when recreating the class, or you create another boolean field "hasMap" and always write that one
Related
I have a Car class implements Parcelable:
public class Car implements Parcelable {
private long id;
private String model;
public Car(long id, String model) {
this.id = id;
this.model = model;
}
public String getModel() {
return model;
}
#Override
public int describeContents() {
return 0;
}
#Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeLong(this.id);
dest.writeString(this.model);
}
private Car(Parcel in) {
this.id = in.readLong();
this.model = in.readString();
}
public static final Creator<Car> CREATOR = new Creator<Car>() {
#Override
public Car createFromParcel(Parcel source) {
return new Car(source);
}
#Override
public Car[] newArray(int size) {
return new Car[size];
}
};
}
Then, I have a Store class which holds a list of cars, it also implements Parcelable:
public class Store implements Parcelable {
private List<Car> carList = new ArrayList<>();
public List<Car> getCarList() {
return carList;
}
public void setCarList(List<Car> cars) {
carList = cars;
}
#Override
public int describeContents() {
return 0;
}
#Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeTypedList(this.carList);
}
private Store(Parcel in) {
in.readTypedList(carList, Car.CREATOR);
}
public static final Creator<Store> CREATOR = new Creator<Store>() {
#Override
public Store createFromParcel(Parcel source) {
return new Store(source);
}
#Override
public Store[] newArray(int size) {
return new Store[size];
}
};
}
In a Fragment, I try to restore the Store instance named myStore, including the list of cars in it:
class MyFragment extends Fragment {
private Store myStore;
#Override
public void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState != null && savedInstanceState.containsKey(MY_STORE)) {
myStore = savedInstanceState.getParcelable(MY_STORE);
// PROBLEM IS HERE:
// when runtime hit here, the restored myStore contains one car but has id with weird long value and null in model field. WHY?
} else {
// initialize myStore with car list (only one car)
Car myCar = new Car(1, "BMW X1");
List<Car> cars = new ArrayList<>();
cars.add(myCar);
myStore = new Store();
myStore.setCarList(cars);
}
}
#Override
public void onSaveInstanceState(#NonNull Bundle outState) {
if(myStore != null) {
outState.putParcelable(MY_STORE, myStore);
}
super.onSaveInstanceState(outState);
}
}
Problem occur when in MyFragment, code tries to restore myStore in onCreate(). At runtime , the restored myStore does contain one car in a list, but the car in that list has id with weird long value (e.g. 28429333427126393) and null in model field. WHY? What did I do wrong? How to restore an object contains a list of another object?
This problem usually occurs when your carList instance was null so you wrote a null object in parcelable and try to read from it.
One of the options is that you initialize your list when it's null and then write it in parcelable. Using this case your list will always have a reference after restoration which might not be what you're looking for.
The other approach is to write a byte value before writing your list into parcelable indicating that your list was null or you should expect some value from it. For this case I'm using this helper class which solved my own problem. Based on my needs, I have made some improvements. Here is my version if you'd prefer to use it:
public class ParcelableUtils {
public static void write(Parcel dest, String string) {
dest.writeByte((byte) (string == null ? 0 : 1));
if (string != null) {
dest.writeString(string);
}
}
public static String readString(Parcel source) {
if (source.readByte() == 1) {
return source.readString();
}
return null;
}
public static void write(Parcel dest, Parcelable parcelable, int flags) {
dest.writeByte((byte) (parcelable == null ? 0 : 1));
if (parcelable != null) {
dest.writeParcelable(parcelable, flags);
}
}
public static <T extends Parcelable> T readParcelable(Parcel source) {
if (source.readByte() == 1) {
return source.readParcelable(null);
}
return null;
}
public static <T extends Parcelable> T readParcelable(Parcel source, Class objectClass) {
if (source.readByte() == 1) {
return source.readParcelable(objectClass.getClassLoader());
}
return null;
}
public static void write(Parcel dest, Map<String, String> strings) {
if (strings == null) {
dest.writeInt(-1);
}
{
dest.writeInt(strings.keySet().size());
for (String key : strings.keySet()) {
dest.writeString(key);
dest.writeString(strings.get(key));
}
}
}
public static Map<String, String> readStringMap(Parcel source) {
int numKeys = source.readInt();
if (numKeys == -1) {
return null;
}
HashMap<String, String> map = new HashMap<String, String>();
for (int i = 0; i < numKeys; i++) {
String key = source.readString();
String value = source.readString();
map.put(key, value);
}
return map;
}
public static <T extends Parcelable> void write(Parcel dest, Map<String, T> objects, int flags) {
if (objects == null) {
dest.writeInt(-1);
} else {
dest.writeInt(objects.keySet().size());
for (String key : objects.keySet()) {
dest.writeString(key);
dest.writeParcelable(objects.get(key), flags);
}
}
}
public static <T extends Parcelable> Map<String, T> readParcelableMap(Parcel source) {
int numKeys = source.readInt();
if (numKeys == -1) {
return null;
}
HashMap<String, T> map = new HashMap<String, T>();
for (int i = 0; i < numKeys; i++) {
String key = source.readString();
T value = source.readParcelable(null);
map.put(key, value);
}
return map;
}
public static void write(Parcel dest, URL url) {
dest.writeString(url.toExternalForm());
}
public static URL readURL(Parcel source) {
try {
return new URL(source.readString());
} catch (MalformedURLException e) {
e.printStackTrace();
}
return null;
}
public static void write(Parcel dest, Date date) {
dest.writeByte((byte) (date == null ? 0 : 1));
if (date != null) {
dest.writeLong(date.getTime());
}
}
public static Date readDate(Parcel source) {
if (source.readByte() == 1) {
return new Date(source.readLong());
}
return null;
}
public static <T extends java.lang.Enum<T>> void write(Parcel dest, java.lang.Enum<T> enu) {
if (enu == null) {
dest.writeString("");
} else {
dest.writeString(enu.name());
}
}
public static <T extends java.lang.Enum<T>> T readEnum(Parcel dest, Class<T> clazz) {
String name = dest.readString();
if ("".equals(name)) {
return null;
}
return java.lang.Enum.valueOf(clazz, name);
}
public static void write(Parcel dest, boolean bool) {
dest.writeByte((byte) (bool ? 1 : 0));
}
public static boolean readBoolean(Parcel source) {
return source.readByte() == 1;
}
public static <T extends Parcelable> void write(Parcel dest,
List<T> fields, int flags) {
if (fields == null) {
dest.writeInt(-1);
} else {
dest.writeInt(fields.size());
for (T field : fields) {
dest.writeParcelable(field, flags);
}
}
}
#SuppressWarnings("unchecked")
public static <T extends Parcelable> List<T> readParcelableList(
Parcel source) {
int size = source.readInt();
if (size == -1) {
return null;
}
ArrayList<T> list = new ArrayList<T>();
for (int i = 0; i < size; i++) {
list.add((T) source.readParcelable(null));
}
return list;
}
}
Hope it helps.
I have a Parcelable object that has a list of Parcelable objects. I am trying to read that list back after it has been passed from one Activity to the next, but only the first element is "un-bundled"
public class MyBundle implements Parcelable {
private List<Data> dataList;
public static final Parcelable.Creator<MyBundle> CREATOR = new Parcelable.Creator<MyBundle>() {
public MyBundle createFromParcel(Parcel in) {
return new MyBundle(in);
}
public MyBundle[] newArray(int size) {
return new MyBundle[size];
}
};
public MyBundle() {
}
public MyBundle(Parcel in) {
//dataList = new ArrayList<>();
//in.readTypedList(dataList, Data.CREATOR);
dataList = in.createTypedArrayList(Data.CREATOR);
//BOTH have the same result
}
#Override
public int describeContents() {
return 0;
}
#Override
public void writeToParcel(Parcel dest, int flags) {
if (dataList != null && dataList.size() > 0) {
dest.writeTypedList(dataList);
}
}
}
The data object:
/*BaseObject has the following properties:
UUID uuid;
long databaseId;
createdDate;
modifiedDate;
*/
public class Data extends BaseObject implements Parcelable {
private String name;
private String serial;
private String location;
public Data() {}
private Data(Parcel in) {
String uuidString = in.readString();
if (uuidString == null) return; //this is null!
uuid = UUID.fromString(idString);
databaseId = in.readLong();
createdDate = new Date(in.readLong());
modifiedDate = new Date(in.readLong());
location = in.readString();
name = in.readString();
serial = in.readString();
}
#Override
public int describeContents() {
return 0;
}
#Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(uuid.toString());
dest.writeLong(databaseId);
dest.writeLong(createdDate.getTime());
dest.writeLong(modifiedDate.getTime());
dest.writeString(name);
dest.writeString(serial);
}
public static final Parcelable.Creator<Data> CREATOR
= new Parcelable.Creator<Data>() {
public Data createFromParcel(Parcel in) {
return new Data(in);
}
public Data[] newArray(int size) {
return new Data[size];
}
};
}
What I have tried:
Debugging - I can see the first element is read fine but the rest are return null, and they do have values when they are being written
"Android, How to use readTypedList method correctly in a Parcelable class?"
"how to properly implement Parcelable with an ArrayList?"
So this is the answer: My Data parcelable misses the location element when it creates the parcel. This obviously results in some kind of offset error when READING occurs. So the coded solution is as follows:
#Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(uuid.toString());
dest.writeLong(databaseId);
dest.writeLong(createdDate.getTime());
dest.writeLong(modifiedDate.getTime());
dest.writeString(location); /*HERE!*/
dest.writeString(name);
dest.writeString(serial);
}
I hope this helps someone else.
My class ExpenseFB, which implements Parcelable, contains a Map of UserFB (which implements Parcelable too):
ExpenseFB:
public class ExpenseFB implements Parcelable {
private String id;
private String name;
private String description;
private String whopaidID;
private String whopaidName;
private Double amount;
private Map<String, UserFB> partecipants;
// setters and getters...
#Override
public int describeContents() {
return 0;
}
#Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(id);
dest.writeString(name);
dest.writeString(description);
dest.writeString(whopaidID);
dest.writeString(whopaidName);
dest.writeMap(partecipants);
}
protected ExpenseFB(Parcel in) {
id = in.readString();
name = in.readString();
description = in.readString();
whopaidID = in.readString();
whopaidName = in.readString();
in.readMap(partecipants,UserFB.class.getClassLoader());
}
public static final Creator<ExpenseFB> CREATOR = new Creator<ExpenseFB>() {
#Override
public ExpenseFB createFromParcel(Parcel in) {
return new ExpenseFB(in);
}
#Override
public ExpenseFB[] newArray(int size) {
return new ExpenseFB[size];
}
};
}
UserFB:
public class UserFB implements Parcelable{
private String id;
private String name;
private String email;
private Map<String, GroupFB> groups;
private Map<String, UserFB> friends;
// setters and getters
#Override
public int describeContents() {
return 0;
}
#Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(id);
dest.writeString(name);
dest.writeString(email);
dest.writeMap(groups);
dest.writeMap(friends);
}
protected UserFB(Parcel in) {
id = in.readString();
name = in.readString();
email = in.readString();
in.readMap(groups,GroupFB.class.getClassLoader());
in.readMap(friends,UserFB.class.getClassLoader());
}
public static final Creator<UserFB> CREATOR = new Creator<UserFB>() {
#Override
public UserFB createFromParcel(Parcel in) {
return new UserFB(in);
}
#Override
public UserFB[] newArray(int size) {
return new UserFB[size];
}
};
}
I want to pass an ExpenseFB object between two Activities by adding the object
ExpenseFB to the intent:
intent.putExtra("id", expenseFB);
When, in debug mode, I execute getIntent().getParcelableExtra("id") in the second activity it raises the following exception when tries to do the readMap() method on the partecipants map:
... Caused by: java.lang.NullPointerException: Attempt to invoke interface method 'java.lang.Object java.util.Map.put(java.lang.Object, java.lang.Object)' on a null object reference
I see that the partecipants map in the first activity is filled: I think that the problem is in the writeMap() method.
Does exist a standard or better way to pass a Parcelable object containing a Map?
Have I to call another method to parcel the Map?
I don't want to use Serializable object because I read that they make worse performances.
The problem is that readMap() is used to read data from a Parcel into and existing Map. You haven't created the Map before calling readMap(), so you get the NullPointerException.
You can solve this by initializing the map when you declare it:
private Map<String, GroupFB> groups = new HashMap<String, GroupFB>();
private Map<String, UserFB> friends = new HashMap<String, UserFB>();
Or, you can create the empty Map in the UserFB constructor, like this:
protected UserFB(Parcel in) {
id = in.readString();
name = in.readString();
email = in.readString();
groups = new HashMap<String, GroupFB>();
in.readMap(groups,GroupFB.class.getClassLoader());
friends = new HashMap<String, UserFB>()
in.readMap(friends,UserFB.class.getClassLoader());
}
You got the point but I think you need to know how to write Map<> into parcelable
Pasting writeParcel() method
#Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(this.groups.size());
for (Map.Entry<String, GroupFb> entry : this.groups.entrySet()) {
dest.writeString(entry.getKey());
dest.writeParcelable(entry.getValue(), flags);
}
}
protected UserFB (Parcel in) {
int groupsSize = in.readInt();
this.groups = new HashMap<String, GroupFb>(groupsSize);
for (int i = 0; i < groupsSize; i++) {
String key = in.readString();
GroupFb value = in.readParcelable(GroupFb.class.getClassLoader());
this.groups.put(key, value);
}
}
Do the same for another Map<> too.
I have a class with the following structure:
class Example {
Integer age;
String name;
Collection<Example2>examples;
class Example2 {
Integer number;
Collection<String>strings;
}
}
How can I make this class implement Parcelable so that I can send its object across activities.
The simplest way will be to use #AutoParcel and let it handle the heavy lifting for you.
You should take a look at this thread : Android: Making a class parcelable
So your classes should be something like :
class Example implements Parcelable {
Integer age;
String name;
ArrayList<Example2> examples;
public Example(Parcel in) {
examples = new ArrayList<>();
readFromParcel(in);
}
#Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(age);
dest.writeString(name);
dest.writeList(examples != null ? examples : new ArrayList());
}
private void readFromParcel(Parcel in) {
age = in.readInt();
name = in.readString();
in.readList(examples, Example2.class.getClassLoader());
}
public static final Creator<Example> CREATOR = new Creator<Example>() {
#Override
public Example createFromParcel(Parcel in) {
return new Example(in);
}
#Override
public Example[] newArray(int size) {
return new Example[size];
}
};
}
and
class Example2 implements Parcelable {
Integer number;
ArrayList<String> labels;
public Example2(Parcel in) {
labels = new ArrayList<>();
readFromParcel(in);
}
#Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInteger(number);
dest.writeList(labels != null ? labels : new ArrayList());
}
#Override
public int describeContents() {
return 0;
}
private void readFromParcel(Parcel in) {
number = in.readInteger();
in.readStringList(labels);
}
public static final Creator<Example2> CREATOR = new Creator<Example2>() {
#Override
public Example2 createFromParcel(Parcel in) {
return new Example2(in);
}
#Override
public Example2[] newArray(int size) {
return new Example2[size];
}
};
}
I could not find any solution for how to treat with integer array in case of parcel (I want to use these two functions dest.writeIntArray(storeId); and in.readIntArray(storeId);).
Here is my code
public class ResponseWholeAppData implements Parcelable {
private int storeId[];
public int[] getStoreId() {
return storeId;
}
public void setStoreId(int[] storeId) {
this.storeId = storeId;
}
#Override
public int describeContents() {
return 0;
}
public ResponseWholeAppData(){
storeId = new int[2];
storeId[0] = 5;
storeId[1] = 10;
}
public ResponseWholeAppData(Parcel in) {
if(in.readByte() == (byte)1)
in.readIntArray(storeId); //how to do this storeId=in.readIntArray(); ?
}
}
#Override
public void writeToParcel(Parcel dest, int flags) {
if(storeId!=null&&storeId.length>0)
{
dest.writeByte((byte)1);
dest.writeIntArray(storeId);
}
else
dest.writeByte((byte)0);
}
public static Parcelable.Creator<ResponseWholeAppData> getCreator() {
return CREATOR;
}
public static void setCreator(Parcelable.Creator<ResponseWholeAppData> creator) {
CREATOR = creator;
}
public static Parcelable.Creator<ResponseWholeAppData> CREATOR = new Parcelable.Creator<ResponseWholeAppData>()
{
public ResponseWholeAppData createFromParcel(Parcel in)
{
return new ResponseWholeAppData(in);
}
public ResponseWholeAppData[] newArray(int size)
{
return new ResponseWholeAppData[size];
}
};
}
When I use "in.readIntArray(storeID)", I get an error:
"Caused by: java.lang.NullPointerException
at android.os.Parcel.readIntArray(Parcel.java:672)".
Instead of using "readIntArray" I used the following:
storeID = in.createIntArray();
Now there are no errors.
I assume class MyObj implements Parcelable and implements all the required methods; I will suggest here only the details about reading / writing parcel.
If array size is known beforehand:
public void writeToParcel(Parcel out, int flags) {
super.writeToParcel(out, flags);
out.writeIntArray(mMyIntArray); // In this example array length is 4
}
protected MyObj(Parcel in) {
super(in);
mMyIntArray = new int[4];
in.readIntArray(mMyIntArray);
}
Otherwise:
public void writeToParcel(Parcel out, int flags) {
super.writeToParcel(out, flags);
out.writeInt(mMyArray.length); // First write array length
out.writeIntArray(mMyIntArray); // Then array content
}
protected MyObj(Parcel in) {
super(in);
mMyIntArray = new int[in.readInt()];
in.readIntArray(mMyIntArray);
}