Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android - polymorphism and Parcelable

I can't figure out how to use polymorohism when writing/reading from parcel. I understand that I need to implement Parcelable in base class, and also in all derived classes (in the case that subclasses have additional properties that I would want to write into parcel). What I don't understand and what I don't know if it's even possible is - how to read subclass from Parcelable i.e., how do I know what kind of subclass I am reading from parcel. I can do some hack like writing some indicator into parcel that would tell me what class loader to use, but I thought there would be some more elegant way, otherwise there is no much use of polymorphism.

To illustrate my question, let say I have classes like this:

Shape.class

public class Shape implements Parcelable {
public float area;

public Shape() {
}

public Shape(Parcel in) {
    area = in.readFloat();
}

public void writeToParcel(Parcel dest, int flags) {
    dest.writeFloat(area);
    }
//CREATOR etc..
}

RectangleShape.class

 public class RectangleShape extends Shape {

    float a;
    float b;

    public RectangleShape(Parcel in) {
        super(in);
        a = in.readFloat();
        b = in.readFloat();
    }

    public void writeToParcel(Parcel dest, int flags) {
        dest.writeFloat(area);
        dest.writeFloat(a);
        dest.writeFloat(b);
    }

//CREATOR etc..
}

CircleShape.class

public static class CircleShape extends Shape {
        float r;

    public CircleShape(Parcel in) {
        super(in);
        r = in.readFloat();
    }

    public void writeToParcel(Parcel dest, int flags) {
        dest.writeFloat(area);
        dest.writeFloat(r);
    }
//CREATOR etc..
 }

Now, in some other class I have something like this:

 public  class Geometry implements Parcelable {

    Shape myShape; 

    public Geometry (boolean condition) {

        if (condition) 
            myShape = new CircleShape(4.5);
        else
            myShape = new RectangleShape(3.4, 3.5);
    }


    @Override
    public Geometry(Parcel in) {
        in.readParcelable(??? - **how do I know what class loader to put here?** )
     }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeParcelable(myShape,flags);
    }
//CREATOR etc..
}

Is there any way that i can "tell" android when writing into parcel what subclass it is, without implicit check of instance type?

like image 768
daneejela Avatar asked Oct 30 '22 18:10

daneejela


1 Answers

I wrote this little fragment and loaded it into a FrameLayout in the main activity to check - and I got positive results (the rebuilt object from the parcel was of type DerivedClass although the references of the object were all of type ObjectClass).

import android.app.Fragment;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.Nullable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

public class Fragment2 extends Fragment {

    public ObjectClass obj;

    public static final String OBJECT_KEY = "Object key";

    public static Fragment2 newInstance(){
        Fragment2 fragment = new Fragment2();
        Bundle args = new Bundle();
        ObjectClass obj = new DerivedClass(); //a DerivedClass object
                               //referenced by an ObjectClass reference
        args.putParcelable(OBJECT_KEY, obj);
        fragment.setArguments(args);
        return fragment;
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_main2, container, false);
        if (savedInstanceState == null){
            Bundle args = getArguments();
            obj = args.getParcelable(OBJECT_KEY); //without supplying the ClassLoader
            ((TextView)view.findViewById(R.id.section_label)).setText(obj.getTitle()); 
                     //The text that is displayed is: "Child class"!
        }
        return view;
    }

    //The parent class
    public static class ObjectClass implements Parcelable {
        String title = "Parent class";

        public String getTitle(){
            return title;
        }

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

        @Override
        public void writeToParcel(Parcel dest, int flags) {

        }

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

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

    //The child class
    public static class DerivedClass extends ObjectClass {
        String title2 = "Child class";

        public String getTitle() {
            return title2;
        }

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

        @Override
        public void writeToParcel(Parcel dest, int flags) {

        }

        public static final Parcelable.Creator<DerivedClass> CREATOR = new Parcelable.Creator<DerivedClass>() {
            @Override
            public DerivedClass createFromParcel(Parcel source) {
                return new DerivedClass();
            }

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

And in the main activity, in onCreate(Bundle) method:

getFragmentManager().beginTransaction().replace(R.id.frameContainer, Fragment2.newInstance()).commit();

My thoughts that led me to this check: As android must figure out what class to use when recreating the object from the parcel (which CREATOR to use), I figured it must store this additional information together with the specified information ( specified in the writeToParcel(Parcel, int) method) when putting it into the bundle. To determine this class I thought it can either use the reference's type or use the object itself (e.g. using getClass() on it). If it uses the object type itself that of course means polymorphism is possible. From my check above it looks like that is indeed the case.

Conclusion and a guess for your example: I think for your example, try not to explicitly pass the ClassLoader. Pass null. From the documentation for the parameter of Parcel#readParcelable(ClassLoader):

ClassLoader: A ClassLoader from which to instantiate the Parcelable object, or null for the default class loader.

I didn't try it with your configuration, so I'm not sure it will work in the same way as in my example. Specifically, I'm not sure whether Parcel#readParcelable(ClassLoader=null) actually works in the same manner as Bundle#getParcelable(String). but if my rationale is right - it should (I assume Bundle#getParcelable(String) uses the default ClassLoader as long as Bundle#setClassLoader(ClassLoader) wasn't called).

Please comment and let me know if it worked for you.

like image 197
et_l Avatar answered Nov 09 '22 03:11

et_l