Search code examples
androidgsonretrofitparcelable

List Returned By RetroFit/GSON Seems to Have Quantum Mechanical Properties


I am using RetroFit 1.9 with a GSON converter that has been working well for me so far. Now I am trying to marshall the List of custom Parcelable objects received in the Callback, and I am met with a ClassCastException:

02-02 09:53:49.921 13030-13030/com.example.app E/AndroidRuntime: FATAL EXCEPTION: main
            Process: com.example.app, PID: 13030
            java.lang.ClassCastException: com.google.gson.internal.LinkedTreeMap cannot be cast to android.os.Parcelable
                at android.os.Parcel.writeTypedList(Parcel.java:1166)
                at com.example.common.util.ParcelableUtil.marshall(ParcelableUtil.java:37)
                at com.example.app.service.WearableMessageService$1.success(WearableMessageService.java:130)
                at com.example.app.service.WearableMessageService$1.success(WearableMessageService.java:120)
                at retrofit.CallbackRunnable$1.run(CallbackRunnable.java:45)
                at android.os.Handler.handleCallback(Handler.java:739)
                at android.os.Handler.dispatchMessage(Handler.java:95)
                at android.os.Looper.loop(Looper.java:148)
                at android.app.ActivityThread.main(ActivityThread.java:5417)
                at java.lang.reflect.Method.invoke(Native Method)
                at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726)
                at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)

This is my callback method:

new Callback<List<MyObject>>() {
    @Override
    public void success(List<MyObject> objects, Response response) {
        mByteArray = ParcelableUtil.marshall(objects);
    }

    @Override
    public void success(RetrofitError error) {
        Timber.w("Failed to retrieve events, with message " + error.getMessage());
    }
}

So I added a for loop in the success method, just before I call ParcelableUtil.marshall(), to test why I am getting a list of LinkedTreeMap objects instead of my objects:

for(MyObject object : objects) {
    Timber.d(object.getTitle());
}

Not only does this print the correct title of each object, miraculously the rest of the code works! ParcelableUtil no longer throws an error, and I receive a byte array that I am later able to unmarshall perfectly.

How does a list of LinkedTreeMap objects change to a list of my objects after it is observed in a loop? Why am I getting a list of LinkedTreeMap objects in the first place? What is going on here?


MyObject class:

public class MyObject implements Parcelable {

    @SerializedName("title") private String mTitle;
    @SerializedName("location") private String mLocation;

    @SerializedName("start_date") private Date mStartDate;
    @SerializedName("end_date") private Date mEndDate;

    public MyObject() {
        /* Required empty constructor */
    }

    public Event(Parcel in) {
        mTitle = in.readString();
        mLocation = in.readString();

        mStartDate = new Date(in.readLong());
        mEndDate = new Date(in.readLong());
    }

    public String getTitle() {
        return mTitle;
    }

    public String getLocation() {
        return mLocation;
    }

    public void getStartDate() {
        return mStartDate();
    }

    public void getEndDate() {
        return mEndDate();
    }

    @Override
    public in describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(mTitle);
        dest.writeString(mLocation);

        dest.writeLong(mStartDate.getTime());
        dest.writeLong(mEndDate.getTime());
    }

    public static final Creator<MyObject> CREATOR = new Creator<MyObject>() {
        @Override
        public MyObject createFromParcel(Parcel in) {
            return new MyObject(in);
        }

        @Override
        public MyObject[] newArray(int size) {
            return new MyObject[size];
        }
    };
}

A sample JSON feed for MyObject:

{
    "items": [
        {
            "title":"Object 1"
            "location":"Location 1"
            "start_date":"2016-02-02 15:30:00"
            "end_date":"2016-02-02 19:00:00"
        }
        {
            "title":"Object 2"
            "location":"Location 2"
            "start_date":"2016-02-02 18:00:00"
            "end_date":"2016-02-03 18:00:00"
        }
    ]
}

The GSON TypeAdapterFactory in use:

public class DataAdapterFactory implements TypeAdapterFactory {

    @Override
    public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {

        final TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type);
        final TypeAdapter<JsonElement> elementAdapter = gson.getAdapter(JsonElement.class);

        return new TypeAdapter<T>() {

            @Override
            public void write(JsonWriter out, T value) throws IOException {
                delegate.write(out, value);
            }

            @Override
            public void read(JsonReader in) throws IOException {
                JsonElement jsonElement = elementAdapter.read(in);

                if(jsonElement.isJsonObject()) {
                    JsonObject jsonObject = jsonElement.getAsJsonObject();

                    if(jsonObject.has("items") && jsonObject.get("items".isJsonArray()) {
                        jsonElement = jsonObject.getAsJsonArray("items");
                    }

                    return delegate.fromJsonTree(jsonElement);
                }
            }
        }.nullSafe();
    }
}

Solution

  • Ah, ProGuard, the bane of my existence...

    I dawned on me this morning. ProGuard could have been removing the class, and using the class in the for loop must have told ProGuard that the class was actually needed. I added the following to my ProGuard rules and it now works without cycling through a loop:

    -keep public class com.example.common.model.MyObject
    -keep public class * implements com.example.common.model.MyObject
    -keepclassmembers class com.example.common.model.MyObject {
        <methods>;
    }
    

    I believe in my case I would only need the first line, but I think the other ones will just cover my bases.