We are the Dev Teams of
  • brands
  • ebay_main
  • ebay
  • mobile
<
>
BLOG

Optional Parcelable Values on Android

by Yasin Yildirim

Optional is a very useful concept in order to avoid from null references and other comparison failures. Optional values are officially supported starting from Java 8, but the fact that Android is (mostly) stuck on Java 7, gives us no chance to use the built-in Optionals on Android code.

There are, of course, third party libraries, like Guava, which implements the Optional concept for older Java versions which gives Android developers a good chance to use Optionals. But, how about moving those objects around between Activities, Fragments and other parts of our Android apps?

In order to move objects and values around on Android, we usually need to store those objects and values in a Bundle or in an Intent. Bundle is a very similar data structure to the usual Map<K,V> which stores key-value pairs, but different than a Map, a Bundle's key can only be a String and the value can be only primitives or certain object types. When a Bundle is passed from one place to another, all key-value pairs in the Bundle are parcelled (or serialized) and restored back again. Parcelable is a nice little interface which helps Android to serialize objects faster and more efficiently. Any object implementing Parcelable can be put into a Bundle.

If you have an Optional value, which needs to be passed to different parts of your application, usual implementations of Optional can't be sufficient. Because all the fields in a Parcelable object needs to be Parcelable, otherwise the parcelling process would not work at all. In order to solve this issue, we took a look at the Optional implementation from various sources (mainly Scala based implementation used by our backend team) and created a similar, but Parcelable version of the Optional class, which we called ParcelableOption<T>.

We added the most useful functionality of an Optional and also implemented Parcelable interface. It's a generic class which accepts a Parcelable as the generic argument, thus the signature looks like this: ParcelableOption<T extends Parcelable>.

First idea was quite simple: Take the value, put it into the parcel in writeToParcel method and read it back inside the CREATOR's createFromParcel method.

It looked something like this:

@Override
public void writeToParcel(Parcel dest, int flags) {
    if (value != null) {
        value.writeToParcel(dest, flags);
    }
}

public static final Parcelable.Creator<ParcelableOption<?>> CREATOR = new Parcelable.Creator<ParcelableOption<?>>() {

    public ParcelableOption<?> createFromParcel(final Parcel in) {
        return ParcelableOption.asOption(in.readParcelable(ClassLoader.getSystemClassLoader()));
    }

    public ParcelableOption<?>[] newArray(int size) {
        return new ParcelableOption[size];
    }
};

 

What we got from this was plenty of mixed up objects all over the place, because of the empty Optionals. When an Optional is empty, it stores a null value. We skipped storing the null values inside the parcel, didn't throw any exception or error and simply ignored it. Then, when it came to unparcel it, we were simply getting the next non-empty object from the parcel.

In order to get away with this problem, we introduced another concept where we store a byte in the parcel, along with the value itself, which looked like this:

@Override
public void writeToParcel(Parcel dest, int flags) {
    dest.writeByte((byte) (value != null ? 1 : 0));
    if (value != null) {
        value.writeToParcel(dest, flags);
    }
}

public static final Parcelable.Creator<ParcelableOption<?>> CREATOR = new Parcelable.Creator<ParcelableOption<?>>() {

    public ParcelableOption<?> createFromParcel(final Parcel in) {
        boolean isAvailable = (in.readByte() == 1);
        if (isAvailable) {
            return ParcelableOption.asOption(in.readParcelable(ClassLoader.getSystemClassLoader()));
        } else {
            return ParcelableOption.none();
        }
    }

    public ParcelableOption<?>[] newArray(int size) {
        return new ParcelableOption[size];
    }
};

This approach was slightly better than the first approach, at least we were able to distinguish between empty and non-empty Optionals. When we released this version as a beta, we started to see this type of exceptions in our crash reporting tool:

android.os.BadParcelableException: ClassNotFoundException when unmarshalling

It didn't take too long for us to figure out that the reason behind those was the default systemClassLoader usage. This led us to our final implementation which includes some reflection magic in order to reach the CREATOR field of the value stored in our ParcelableOption. We unparcel the value using its CREATOR field, which obviously is the best place to look at if you want to know how to unparcel a Parcelable.

As we suffered a lot during the absence and implementation of the Parcelable Optional, we decided to share this class with the world, for everyone who wants to benefit from using it.

import android.os.Parcel;
import android.os.Parcelable;
import android.util.Log;

import java.io.Serializable;
import java.lang.reflect.Field;
import java.util.NoSuchElementException;

import static java.lang.String.format;

public final class ParcelableOption<T extends Parcelable> implements Serializable, Parcelable {

    private static final long serialVersionUID = 5621445373601999739L;

    private static final String LOG_TAG = "PARCELABLE_OPTION";

    private static final ParcelableOption<?> NONE = new ParcelableOption(null);

    private final T value;

    private ParcelableOption(T value) {
        this.value = value;
    }

    /**
     * Create an option. Depending on the value an empty or a defined option will be created.
     *
     * @param value The value. {{null}} is allowed.
     * @return An option. If the value is null, an empty option, otherwise an option with a defined value.
     */
    public static <T extends Parcelable> ParcelableOption<T> asOption(T value) {
        return value != null ? some(value) : ParcelableOption.<T>none();
    }

    /**
     * @param value The value, must be not {{null}}!
     * @return An option with a defined value.
     */
    public static <T extends Parcelable> ParcelableOption<T> some(T value) {
        if (value == null) {
            throw new IllegalArgumentException("Null not allowed!");
        }
        return new ParcelableOption<>(value);
    }

    /**
     * @return An option with an empty value.
     */
    public static <T extends Parcelable> ParcelableOption<T> none() {
        return (ParcelableOption<T>) NONE;
    }

    /**
     * @return The value
     * @throws NoSuchElementException if {@link #isEmpty()} is true.
     */
    public T getValue() {
        if (isEmpty()) {
            throw new NoSuchElementException("value is undefined");
        }
        return value;
    }

    /**
     * @return True, if this option has been created with {@link #some(T)}.
     */
    public boolean isAvailable() {
        return value != null;
    }

    /**
     * @return True, if this option has been created with {@link #none()}.
     */
    public boolean isEmpty() {
        return !isAvailable();
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + (isAvailable() ? 0 : value.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }

        if (obj == null) {
            return false;
        }

        if (getClass() != obj.getClass()) {
            return false;
        }

        ParcelableOption<T> other = (ParcelableOption<T>) obj;

        if (isEmpty()) {
            if (other.isAvailable()) {
                return false;
            }
        } else if (!value.equals(other.value)) {
            return false;
        }
        return true;
    }

    /**
     * @param defaultAlternative
     * @return the value, if defined, otherwise the given default.
     */
    public T or(T defaultAlternative) {
        return isAvailable() ? value : defaultAlternative;
    }

    @Override
    public String toString() {
        return isAvailable() ? format("option[%s]", value) : "option[empty]";
    }

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

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeByte((byte) (value != null ? 1 : 0));
        if (value != null) {
            dest.writeString(value.getClass().getName());
            value.writeToParcel(dest, flags);
        }
    }

    /**
     * @see Creator
     */
    public static final Parcelable.Creator<ParcelableOption<?>> CREATOR = new Parcelable.Creator<ParcelableOption<?>>() {

        public ParcelableOption<?> createFromParcel(final Parcel in) {
            boolean isAvailable = (in.readByte() == 1);
            if (isAvailable) {
                try {
                    Class cls = Class.forName(in.readString());

                    Field creatorField = cls.getField("CREATOR");

                    Parcelable value = null;

                    if (creatorField.get(null) instanceof Parcelable.Creator) {
                        value = (Parcelable) ((Parcelable.Creator) creatorField.get(null)).createFromParcel(in);
                    } else {
                        Log.e(LOG_TAG, "CREATOR field is not instance of Parcelable.Creator");
                    }

                    if (value != null) {
                        return ParcelableOption.asOption(value);
                    }
                    Log.e(LOG_TAG, "The value created from parcel is null");
                    return ParcelableOption.none();
                } catch (Exception e) {
                    Log.e(LOG_TAG, "Failed to unparcel", e);
                    return ParcelableOption.none();
                }
            }
            return ParcelableOption.none();
        }

        public ParcelableOption<?>[] newArray(int size) {
            return new ParcelableOption[size];
        }
    };
}
?>