Parcelable
is a marker interface, reminiscent of
Serializable
, that shows up in many places in the Android SDK.
Parcelable
objects can be put into Intent
extras or
Bundle
objects, for example. Making your own custom classes
implement Parcelable
greatly increases their flexibility.
At the same time, Parcelable
is something that can be overused.
In most Android apps, few if any custom classes really need to have
Parcelable
capabilities.
In this chapter, we will review how to modify classes to implement
Parcelable
and what the limitations are on using Parcelable
.
Understanding this chapter requires that you have read the core chapters of this book.
A Parcelable
object is one that can be placed into a Parcel
. A
Parcel
is the primary vehicle for passing data between processes
in Android’s inter-process communication (IPC) framework.
IPC abounds in Android, even in places where you may not expect it.
Every time you call startActivity()
, for example, IPC occurs, even
if the activity that calls startActivity()
and the activity to be
started are in the same process. A core OS process is the one that
is responsible for identifying the activity to be started and routing
control to it, so startActivity()
performs IPC from the original
activity’s process to a core OS process. The core OS process then
eventually performs IPC to the target process for the activity to be
started.
If you see an Intent
or a Bundle
in the Android SDK, odds are
that those objects are involved in IPC. That is not always the
case — LocalBroadcastManager
, for example, uses Intent
objects
purely in-process — but it is a reasonable rule of thumb. Hence,
there is keen interest in being able to implement Parcelable
on
specific classes, either to pass to other components via Intent
extras, or to become part of the saved instance state Bundle
.
Parcelable
objects are also important for use with
remote services via the binding pattern.
You have three major approaches for adding Parcelable
capabilities to your classes in Android:
Parcelable
-enabled rendition as outputEnterprising developers have created annotation processing libraries
that can be used to add Parcelable
capabilities to a Java class
in an Android app.
One approach is used by Parceler.
Here, you just add a @Parcel
annotation to the Java class, and it
code generates what is needed. However, it does not actually make
the Java class Parcelable
. Rather, it creates a runtime wrapper class
that is Parcelable
and that knows how to convert instances of your
own Java class to and from the wrapper. You wind up calling static
wrap()
and unwrap()
methods on a Parcels
class to handle
the conversion between your class and the generated Parcelable
class.
AutoParcel takes a slightly different approach. In this case, you need to:
@AutoParcel
annotation to the classabstract
and have it implement Parcelable
abstract
method signatures for getters for the data membersAutoParcel then code-generates a Java class that implements the getters,
data members, and Parcelable
logic, along with other niceties like
equals()
and hashCode()
. That Java class will be named AutoParcel_
,
followed by the name of the class with the @AutoParcel
annotation
(e.g., annotating a Foo
class gives you an AutoParcel_Foo
class).
The AutoParcel-generated class is a concrete subclass of the abstract
base class, and so you can just work with the abstract class’ public
API and let AutoParcel handle the details.
However, neither of these give you classes that play well with the
other children. Other code that expects to work with your
classes — whether that is passing a Parceler-defined Parcelable
to a third-party app or using something like Gson to handle JSON
parsing — will not like either Parceler or AutoParcel that much.
The Parcelabler Web site is a code generator. You paste in a simple Java class, with the class declaration and data members:
class Book {
String isbn;
String title;
int pubYear;
}
and it gives you an output class that adds the Parcelable
logic:
class Book implements Parcelable {
String isbn;
String title;
int pubYear;
protected Book(Parcel in) {
isbn = in.readString();
title = in.readString();
pubYear = in.readInt();
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(isbn);
dest.writeString(title);
dest.writeInt(pubYear);
}
@SuppressWarnings("unused")
public static final Parcelable.Creator<Book> CREATOR = new Parcelable.Creator<Book>() {
@Override
public Book createFromParcel(Parcel in) {
return new Book(in);
}
@Override
public Book[] newArray(int size) {
return new Book[size];
}
};
}
We will see in the next section what all of that code does for us, as part of understanding how to build it by hand.
However, the Parcelabler Web site has some limitations in its Java
parsing, and so the more complex your Java class, the more likely it
is that the Parcelabler site will have difficulty understanding it and
blending in the Parceable
logic.
The ParcelableCodeGenerator project
implements a command-line code
generator that takes a JSON schema and gives you a Java class that,
among other things, has the Parcelable
implementation.
Adding Parcelable
support yourself is not especially difficult, though
it is a bit tedious.
The first steps is to add implements Parcelable
to the class. Immediately,
your IDE should start complaining that you need to implement two methods
to satisfy the Parcelable
interface.
The easier of the two methods is describeContents()
, where you will
return 0, most likely.
The other method you will need to implement is writeToParcel()
.
You are passed in two parameters: a very important Parcel
, and a
usually-ignored int
named flags
.
Your job, in writeToParcel()
, is to call a series of write...()
methods
on the Parcel
to write out all data members of this object that
should be considered part of the object as it is passed across
process boundaries. There are dozens of type-safe methods for writing
data into the Parcel
:
writeInt()
) or
Java arrays of primitives (e.g., writeStringArray()
)writeBundle()
, for writing out a Bundle
writeParcelable()
and writeParcelableArray()
, for writing
out other objects that implement Parcelable
writeFileDescriptor()
, for putting a FileDescriptor
into the
Parcel
, with an eye towards allowing whoever reconstitutes the
Parcelable
to be able to read or write a stream based on that
FileDescriptor
IBinder
objects from a remote service binding
writeSizeF()
)
or interfaces (e.g., writeSerializable()
)If, in writeToParcel()
, you called writeFileDescriptor()
, you will want
to have describeContents()
return CONTENTS_FILE_DESCRIPTOR
instead
of 0, as apparently the Parcelable
support logic needs to know that
a file descriptor is in the Parcel
.
In the case of the generated Book
code shown earlier in this chapter,
writeToParcel()
writes out the two String
and one int
data member:
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(isbn);
dest.writeString(title);
dest.writeInt(pubYear);
}
When Android tries reading objects in from a Parcel
, and it encounters
an instance of your Parcelable
class, it will retrieve a static
CREATOR
object that must be defined on that class. The CREATOR
is an instance of Parcelable.Creator
, using generics to tie it to the
type of your class:
@SuppressWarnings("unused")
public static final Parcelable.Creator<Book> CREATOR = new Parcelable.Creator<Book>() {
@Override
public Book createFromParcel(Parcel in) {
return new Book(in);
}
@Override
public Book[] newArray(int size) {
return new Book[size];
}
};
The @SuppressWarnings("unused")
annotation is because the IDE will think
that this CREATOR
instance is not referred to anywhere. That is because
it will only be used via Java reflection.
The CREATOR
will need two methods. createFromParcel()
, given a Parcel
,
needs to return an instance of your class populated from that Parcel
.
newArray()
, given a size, needs to return a type-safe array of your
class.
The typical implementation of createFromParcel()
will delegate the
actual work to a protected
or private
constructor on your class that
takes the Parcel
as input:
protected Book(Parcel in) {
isbn = in.readString();
title = in.readString();
pubYear = in.readInt();
}
You need to read in the same values that you wrote out to the
Parcel
, and in the same order.
Android Studio 1.3 and higher have a template for a new Parcelable
class. Right-click over your desired Java package and choose New > Other >
New Parcelable Type from the context menu. Fill in your class and
the template will create a new standalone Java class, akin to this one:
import android.os.Parcel;
import android.os.Parcelable;
public class Item implements Parcelable {
// TODO declare your real class members
// Members must be either primitives, primitive arrays or parcelables
private int mFoo;
private String mBar;
// TODO implement your constructors, getters & setters, methods
private Item(Parcel in) {
// TODO read your class members from the parcel
// Note: order is important - you must read in the same order
// you write in writeToParcel!
mFoo=in.readInt();
mBar=in.readString();
}
@Override
public void writeToParcel(Parcel out, int flags) {
// TODO write your class members to the parcel
// Note: order is important - you must write in the same order
// you read in your private parcelable constructor!
out.writeInt(mFoo);
out.writeString(mBar);
}
@Override
public int describeContents() {
// TODO return Parcelable.CONTENTS_FILE_DESCRIPTOR if your class members
// include a FileDescriptor, otherwise you can simply return 0
return 0;
}
public static final Parcelable.Creator<Item> CREATOR=new Parcelable.Creator<Item>() {
public Item createFromParcel(Parcel in) {
return new Item(in);
}
public Item[] newArray(int size) {
return new Item[size];
}
};
}
You would have to adjust the stock fields (mFoo
, mBar
) to be what
you need, and adjust the writeToParcel()
and private constructor
to match.
However, this template is designed for starting from scratch; it is
not that useful when you have an existing class and wish to now
make it be Parcelable
.
The ParcelablePlease library
saves you from having to do all of the reading and writing to and from
the Parcel
yourself. Putting the @ParceablePlease
annotation on the
class generates a class for you (your class name followed by ParcelablePlease
,
so FooParcelablePlease
for a Foo
class). This class only marshals
your data members to and from a Parcel
, via static readFromParcel()
and writeToParcel()
methods. You still have to have the rest of the
Parcelable
boilerplate. Hence, this library is not as powerful
as the annotation processors mentioned earlier in this chapter, but you
wind up with a “real” complete Java class that can work better with
other annotation-based libraries like Gson. On the other hand, it still
makes it difficult for you to distribute your code to third parties, as
they will need to also have this annotation processing library in their
project builds.
While the mechanics of writing a Parcelable
are not hard, this does
not mean that every model object or other POJO in your app should be
made Parcelable
. Overuse of Parcelable
is a bit of a code smell,
as it suggests that the developer is not necessarily considering all
of the limitations and effects of the use of Parcelable
.
The biggest one (pun lightly intended) is the size limitation. A Parcel
–
the IPC structure that is used to pass Parcelable
objects across
process boundaries — has a 1MB size limit. If you get over this limit,
you will likely crash with a “Failed Binder Transaction” message as part
of the exception’s stack trace.
There are two main ways you can reach this limit:
Parcelable
that individually is too large. A common case for
this is wrapping a Bitmap
or other large byte
array in some
Parcelable
object.Parcelable
objects. For example, you might have performed
a database query, converted the results into a collection of model objects,
then tried to pass that collection to another activity via an Intent
extra. Syntactically, this can work fine, if the collection and its model
objects are all Parcelable
. But now your risk of hitting the 1MB limit
is determined by how many rows there are in the query’s result set, and that
can vary by user.Large data like this need to be managed by singletons or other static
data members and shared among your application components, rather than
passed via Parcelable
objects.
Suppose we have two activities, A and B. Activity A calls startActivity()
, identifying
activity B in the Intent
. The Intent
also includes a custom Parcelable
object, one that takes up 1KB of space.
Question: how much system RAM is taken up by that Parcelable
?
Wrong Answer: 1KB.
Right Answer: At least 3KB, as there are at least three copies of the
Parcelable
data:
Parcelable
object, the one that is stored
as an extra in the Intent
Parcel
that is held by a core OS
process, for handling things like configuration changes and the recent-tasks
list, where that Intent
(and its extras, including your Parcelable
) are
neededIntent
that Activity B receivesParcelable
is, in effect “pass-by-value”, as the Parceable
object
is copied as part of getting it across the process boundary twice,
once from your process to the core OS, and once from the core OS back
to your process.
This means that modifications that Activity B makes to the Parcelable
object will not be seen by Activity A, as they are working on separate
copies of the object. Similarly, changes that Activity B makes to the
Parcelable
will not affect the copy held by the core OS process and
re-delivered to Activity B on a configuration change.
The safest way to help defend against mistakes related to this is to
consider a Parcelable
object to be an immutable
object. Only configure it through a constructor (possibly with the assistance
of some Builder
if you want a cleaner API). Offer getters for the
values in the Parcelable
, but do not offer any setters, so once the
instance is created, it cannot be changed.
Also note that these copies magnify the effects of having a large
Parcelable
object, or too many Parcelable
objects in a Parcel
. A
900KB Parcel
might fit within the 1MB size limit, but it would
consume at least 2.7MB if the Parcel
is part of some IPC.
Conversely, there are cases where Intent
objects are not passed
across process boundaries, such as LocalBroadcastManager
. In those
cases, neither the 1MB limit nor the pass-by-value effect are an issue.
Only if the Intent
is “flattened” into a Parcel
, and later converted
back into an Intent
, do these extra copies and the 1MB limit come into play.
Sometimes, weird stuff happens, particularly when trying to read in
other Parcelable
objects that you wrote to the Parcel
. In this case,
the Parcel
system needs to use Java reflection to find the Java class
associated with the Parcelable
objects, and sometimes it gets a bit
lost.
When you use readParcelable()
to read in the Parcelable
objects
out of the Parcel
, you may need to supply the ClassLoader
that
you know has those Parcelable
classes:
Foo(Parcel in) {
this.someField=in.readParcelable(getClass().getClassLoader());
this.anotherField=in.readParcelable(getClass().getClassLoader());
}
Here, we are using the same ClassLoader
that has this Foo
class.
Parcelable
objects need to read and write the same values to and from
the Parcel
. This sounds simple, but it gets into some nasty issues
when multiple code bases need to work with the Parcelable
.
For example, suppose your app offers an SDK, such as a remote service.
You have some custom Parcelable
objects that you can either give
to third-party clients of your app or get as input from those clients.
Now, your SDK needs to ship implementations of the Parcelable
classes;
without them, clients cannot use you exposed service API.
What happens now, if you change the definition of the Parcelable
? Bear
in mind that:
As a result, it is reasonably likely that your Parcelable
implementations
will be out of sync on a user’s device, with your app having one
implementation and a third-party app having another implementation.
The results of this may not be pretty.
This is not a problem for purely internal uses of Parcelable
, such
as for holding onto data across a configuration change.
Custom Parcelable
objects are fine for use as extras in Intent
objects
used with LocalBroadcastManager
or otherwise limited to your own
process. Custom Parcelable
objects should work when placed in the
saved instance state Bundle
.
The further you get from these scenarios,
though, the more likely it is that you will run into cases where your
custom Parcelable
will cause problems. That is because other apps — and
core OS processes — have no access to your Parcelable
class. Any
attempt to work with Intent
extras will result in a crash in that
other process, probably interrupting whatever it was that you were
trying to do.
One example of this is using a custom Parcelable
in an extra for an
Intent
, wrapped in a PendingIntent
. The party that executes the
PendingIntent
has the ability to add extras to the Intent
inside
the PendingIntent
. That, in turn, causes problems, as Android does not
have access to your Parcelable
class. You get a stack trace like this
one:
E/Parcel: Class not found when unmarshalling: com.commonsware.android.parcelable.marshall.Thingy
java.lang.ClassNotFoundException: com.commonsware.android.parcelable.marshall.Thingy
at java.lang.Class.classForName(Native Method)
at java.lang.Class.forName(Class.java:400)
at android.os.Parcel.readParcelableCreator(Parcel.java:2507)
at android.os.Parcel.readParcelable(Parcel.java:2461)
at android.os.Parcel.readValue(Parcel.java:2364)
at android.os.Parcel.readArrayMapInternal(Parcel.java:2717)
at android.os.BaseBundle.unparcel(BaseBundle.java:269)
at android.os.Bundle.putAll(Bundle.java:226)
at android.content.Intent.fillIn(Intent.java:8171)
at com.android.server.am.PendingIntentRecord.sendInner(PendingIntentRecord.java:255)
at com.android.server.am.PendingIntentRecord.sendWithResult(PendingIntentRecord.java:216)
at com.android.server.am.ActivityManagerService.sendIntentSender(ActivityManagerService.java:7151)
at android.app.PendingIntent.send(PendingIntent.java:836)
at com.android.server.AlarmManagerService$DeliveryTracker.deliverLocked(AlarmManagerService.java:2984)
at com.android.server.AlarmManagerService.deliverAlarmsLocked(AlarmManagerService.java:2424)
at com.android.server.AlarmManagerService$AlarmThread.run(AlarmManagerService.java:2543)
One workaround for cases like this is to still use a custom Parcelable
,
but instead of putting it directly as an Intent
extra, use the Parcel
system to convert it into a byte array, and store that as the extra.
Foreign processes have no idea what the byte array is for and will
not try to convert it into anything. When you get the byte array, you can
then use the Parcel
system to get your Parcelable
back.
The
Parcelable/Marshall
sample project demonstrates this technique. It is a clone of the
EventBus/GreenRobot3
sample app, discussed in
the chapter on event buses. The app uses AlarmManager
to get control every minute, posting an event on a greenrobot EventBus.
That event adds a row in a ListView
if the UI is in the foreground;
otherwise, the WakefulIntentService
triggered by the alarm event will
show a Notification
.
This sample app does not really need a custom Parcelable
. However, lots
of programs have lots of things that they do not really need. So, the
Parcelable/Marshall
project adds a custom Parcelable
class,
named Thingy
:
package com.commonsware.android.parcelable.marshall;
import android.os.Parcel;
import android.os.Parcelable;
public class Thingy implements Parcelable {
final String something;
final int anotherThing;
public Thingy(String something, int anotherThing) {
this.something=something;
this.anotherThing=anotherThing;
}
protected Thingy(Parcel in) {
something=in.readString();
anotherThing=in.readInt();
}
@Override
public int describeContents() {
return(0);
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(something);
dest.writeInt(anotherThing);
}
@SuppressWarnings("unused")
public static final Parcelable.Creator<Thingy> CREATOR=
new Parcelable.Creator<Thingy>() {
@Override
public Thingy createFromParcel(Parcel in) {
return(new Thingy(in));
}
@Override
public Thingy[] newArray(int size) {
return(new Thingy[size]);
}
};
}
It is fairly vanilla Parcelable
class, wrapped around a string and
an integer.
In theory, we could put a Thingy
into the Intent
used with
AlarmManager
via a PendingIntent
. However, that will run into the
problem outlined in this section, as Android does not have a Thingy
.
In fact, the stack trace shown above comes from this sample project,
if you try putting a Thingy
into the Intent
.
The revised version of the project instead puts a byte array in the
Intent
as an extra, by way of the Parcelables
utility class:
package com.commonsware.android.parcelable.marshall;
import android.os.Parcel;
import android.os.Parcelable;
// inspired by http://stackoverflow.com/a/18000094/115145
public class Parcelables {
public static byte[] toByteArray(Parcelable parcelable) {
Parcel parcel=Parcel.obtain();
parcelable.writeToParcel(parcel, 0);
byte[] result=parcel.marshall();
parcel.recycle();
return(result);
}
public static <T> T toParcelable(byte[] bytes,
Parcelable.Creator<T> creator) {
Parcel parcel=Parcel.obtain();
parcel.unmarshall(bytes, 0, bytes.length);
parcel.setDataPosition(0);
T result=creator.createFromParcel(parcel);
parcel.recycle();
return(result);
}
}
Parcelables
mirrors standard Java utility classes like Arrays
(static
utility methods for Java arrays) and Collections
(static
utility methods for subclasses of Collection
). Parcelables
has two static
utility methods for working with Parcelable
objects:
toByteArray()
converts the Parcelable
to a byte arraytoParcelable()
converts the byte array back into a Parcelable
Both work by way of a Parcel
object. You can get one of these from
an instance pool by calling the static obtain()
method on Parcel
.
toByteArray()
gets a Parcel
, uses writeToParcel()
to put your
Parcelable
into the Parcel
, then uses marshall()
to get a
byte array representation of the Parcel
contents. Before returning
that result, though, we recycle()
the Parcel
, returning it to the
instance pool for later use.
toParcelable()
needs not only the byte array representing your object,
but also your Parcelable.Creator
, which knows how to convert a Parcel
back into your Parcelable
. So, toParcelable()
:
Parcel
via obtain()
unmarshall()
to populate the Parcel
with the byte array contentssetDataPosition(0)
to effectively “rewind” the Parcel
back
to the beginningcreateFromParcel()
on your Parcelable.Creator
to get the
Parcelable
out of the Parcel
Parcel
Parcelable
The scheduleAlarms()
method on PollReceiver
is responsible for creating
the Intent
to schedule some alarm events. It adds a Thingy
to the
Intent
, but uses toByteArray()
to add the extra, rather than putting
the raw Thingy
in as the extra:
static void scheduleAlarms(Context ctxt) {
AlarmManager mgr=
(AlarmManager)ctxt.getSystemService(Context.ALARM_SERVICE);
Thingy thingy=
new Thingy(mgr.getClass().getCanonicalName(), mgr.hashCode());
Intent i=
new Intent(ctxt, PollReceiver.class)
.putExtra(EXTRA_THINGY, Parcelables.toByteArray(thingy));
PendingIntent pi=PendingIntent.getBroadcast(ctxt, 0, i, 0);
mgr.setRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
SystemClock.elapsedRealtime() + INITIAL_DELAY,
PERIOD, pi);
}
When the alarm event occurs, PollReceiver
can get the Thingy
back
by retrieving the byte array extra and using toParcelable()
:
@Override
public void onReceive(Context ctxt, Intent i) {
Thingy thingy=
Parcelables.toParcelable(i.getByteArrayExtra(EXTRA_THINGY),
Thingy.CREATOR);
if (i.getAction() == null) {
ScheduledService.enqueueWork(ctxt);
}
else {
scheduleAlarms(ctxt);
}
}
While this approach will add a few lines of code to your project, it
should not incur significant additional overhead. All the work that
is being done here is part and, um, parcel of passing a Parcelable
between processes anyway. We are just doing it proactively, to eliminate
any references in the Parcel
to our custom Parcelable
class.