Hey everyone,
For this post, I thought I’d revisit the topic of Intents. In the past I’ve talked about passing Intents in between classes with simple, primitive data (see Passing Information between Activities) – turns out it’s a relatively easy task, and a core concept within the Android framework. However, the much harder task is passing your own classes (objects) in between Activities, and moreover, potentially passing lists of objects.
In this post, I’ll go through an example of writing an object that implements the Parcelable interface.
Consider a scenario where you have a list of Venues (i.e. Restaurants, Hotels, Clubs, anything with fields for lat, long, name, address, etc). Your user makes a request and gets back a list of these Venues. You then want to pass these results to a new Activity – maybe it’s a mapping Activity (as it was in my case), but regardless the goal is to pass this list of Venues from Activity A to Activity B without having to make the same request twice.
The first step is defining your object. The object must implement the Parcelable interface. Why you might ask? Well let’s step back and think about what’s happening here. Within Android, much of the inter-process (inter-Activity) communication is done through light weight data structures known as Parcels. In fact, both Intents and Bundles (two objects you’ve probably encountered before…) implement this Parcelable interface.
But how does this inter-process communication work? An Android process first converts an object into a byte stream which is then sent to another process (i.e. another Activity). This second process then reads in the byte stream and converts it back into an object: this exchange is more commonly known as serialization or marshalling. But how do the two Activities know what to do to serialize and de-serialize your object? For primitives like ints, doubles, even Strings, serialization is trivial as these primitives are already in byte form. And so this is where the Parcelable interface comes in.
By implementing the Parcelable interface, you are essentially giving the OS instructions on how to serialize and de-serialize your object. Conceptually this may be difficult to picture, but luckily Android has made the code for this super simply – in fact you are only required to override a few methods. With that, let’s take a look at what exactly needs to be done in the implementation:
For this post, I thought I’d revisit the topic of Intents. In the past I’ve talked about passing Intents in between classes with simple, primitive data (see Passing Information between Activities) – turns out it’s a relatively easy task, and a core concept within the Android framework. However, the much harder task is passing your own classes (objects) in between Activities, and moreover, potentially passing lists of objects.
In this post, I’ll go through an example of writing an object that implements the Parcelable interface.
Consider a scenario where you have a list of Venues (i.e. Restaurants, Hotels, Clubs, anything with fields for lat, long, name, address, etc). Your user makes a request and gets back a list of these Venues. You then want to pass these results to a new Activity – maybe it’s a mapping Activity (as it was in my case), but regardless the goal is to pass this list of Venues from Activity A to Activity B without having to make the same request twice.
The first step is defining your object. The object must implement the Parcelable interface. Why you might ask? Well let’s step back and think about what’s happening here. Within Android, much of the inter-process (inter-Activity) communication is done through light weight data structures known as Parcels. In fact, both Intents and Bundles (two objects you’ve probably encountered before…) implement this Parcelable interface.
But how does this inter-process communication work? An Android process first converts an object into a byte stream which is then sent to another process (i.e. another Activity). This second process then reads in the byte stream and converts it back into an object: this exchange is more commonly known as serialization or marshalling. But how do the two Activities know what to do to serialize and de-serialize your object? For primitives like ints, doubles, even Strings, serialization is trivial as these primitives are already in byte form. And so this is where the Parcelable interface comes in.
By implementing the Parcelable interface, you are essentially giving the OS instructions on how to serialize and de-serialize your object. Conceptually this may be difficult to picture, but luckily Android has made the code for this super simply – in fact you are only required to override a few methods. With that, let’s take a look at what exactly needs to be done in the implementation:
public
class
ParcelableVenue
implements
Parcelable {
private
double
lat, lon;
private
String name, address;
public
ParcelableVenue(
double
lat,
double
lon, String name, String address) {
this
.lat = lat;
this
.lon = lon;
this
.name = name;
this
.address = address;
}
public
ParcelableVenue(Parcel source) {
// TODO implement
}
public
GeoPoint getGeoPoint() {
return
new
GeoPoint((
int
) (lat * 1e6), (
int
) (lon * 1e6));
}
public
String getName() {
return
name;
}
public
String getAddress() {
return
address;
}
@Override
public
int
describeContents() {
// OVERRIDE METHOD #1
return
0
;
}
@Override
public
void
writeToParcel(Parcel dest,
int
flags) {
// OVERRIDE METHOD #2
dest.writeDouble(
this
.lat);
dest.writeDouble(
this
.lon);
dest.writeString(
this
.name);
dest.writeString(
this
.address);
}
public
static
final
Parcelable.Creator<ParcelableVenue> CREATOR =
new
Parcelable.Creator<ParcelableVenue>() {
// TODO implement
};
}
So far we have the basic structure of our ParcelableVenue object – it
has a few simple fields as well as some standard getters; nothing
special there. You’ll then notice that there are two methods we need to
override. The first is the describeContents() method. Typically
returning 0 suffices unless you have numerous parcelable objects and
require special serialization for some. The method itself is meant to
return a bit mask that identifies the serialized object. In my case, I
just return 0.
The second method is the writeToParcel(Parcel dest, int flags) method. The meat of the conversion happens here. In this method you are passed a destination Parcel which is eventually serialized and sent to the end process. Thus you simply need to write your object’s data into this parcel. Luckily, some simple write methods are given to you, such as writeDouble(), writeString(), writeInt(), writeIntArray(), etc. The flags parameter simply tells the writeToParcel() method how the object should be written.
Once these two methods are overridden, every class that implements the Parcelable interface then needs to have a static Parcelable.Creator object named CREATOR. Let’s step back one more time – where are we at this point? Well so far we’ve flattened the object and written it to a Parcel object. Our object is in essence nothing but a byte stream now, so the only thing that’s left to do is un-flatten it and convert it back into an object using this CREATOR object!
Our creator object is pretty simple and need only look like:
The order here is important – the first double read will be the first double written as, again, it is a byte stream.
And that’s it! Once we have our ParcelableVenue, we can then do things like:
The second method is the writeToParcel(Parcel dest, int flags) method. The meat of the conversion happens here. In this method you are passed a destination Parcel which is eventually serialized and sent to the end process. Thus you simply need to write your object’s data into this parcel. Luckily, some simple write methods are given to you, such as writeDouble(), writeString(), writeInt(), writeIntArray(), etc. The flags parameter simply tells the writeToParcel() method how the object should be written.
Once these two methods are overridden, every class that implements the Parcelable interface then needs to have a static Parcelable.Creator object named CREATOR. Let’s step back one more time – where are we at this point? Well so far we’ve flattened the object and written it to a Parcel object. Our object is in essence nothing but a byte stream now, so the only thing that’s left to do is un-flatten it and convert it back into an object using this CREATOR object!
Our creator object is pretty simple and need only look like:
public
class
ParcelableVenue
implements
Parcelable {
private
double
lat, lon;
private
String name, address;
public
ParcelableVenue(
double
lat,
double
lon, String name, String address) {
this
.lat = lat;
this
.lon = lon;
this
.name = name;
this
.address = address;
}
public
ParcelableVenue(Parcel source) {
// TODO implement
}
// ...
@Override
public
void
writeToParcel(Parcel dest,
int
flags) {
dest.writeDouble(
this
.lat);
dest.writeDouble(
this
.lon);
dest.writeString(
this
.name);
dest.writeString(
this
.address);
}
public
static
final
Parcelable.Creator<ParcelableVenue> CREATOR =
new
Parcelable.Creator<ParcelableVenue>() {
@Override
public
ParcelableVenue createFromParcel(Parcel source) {
return
new
ParcelableVenue(source);
// RECREATE VENUE GIVEN SOURCE
}
@Override
public
ParcelableVenue[] newArray(
int
size) {
return
new
ParcelableVenue[size];
// CREATING AN ARRAY OF VENUES
}
};
}
And so we see that the very last step is simply to write another
constructor for our ParcelableVenue class which initializes an object
given a Parcel. This can be done with:
public
class
ParcelableVenue
implements
Parcelable {
private
double
lat, lon;
private
String name, address;
// ...
public
ParcelableVenue(Parcel source) {
this
.lat = source.readDouble();
this
.lon = source.readDouble();
this
.name = source.readString();
this
.address = source.readString();
}
// ...
}
And that’s it! Once we have our ParcelableVenue, we can then do things like:
public
class
ClubsListActivity
extends
ListActivity {
private
List<Club> clubs;
private
Button mapViewButton;
@Override
protected
void
onCreate(Bundle savedInstanceState) {
super
.onCreate(savedInstanceState);
setContentView(R.layout.club_list);
mapViewButton = (Button) findViewById(R.id.switch_map_view);
mapViewButton.setOnClickListener(
new
OnClickListener() {
@Override
public
void
onClick(View v) {
// PREPARE NEW INTENT TO SEND TO MAP ACTIVITY
Intent i =
new
Intent(ClubsListActivity.
this
, VenueMapView.
class
);
// INITIALIZE NEW ARRAYLIST AND POPULATE
ArrayList<ParcelableVenue> overlays =
new
ArrayList<ParcelableVenue>();
for
(Club c : clubs) {
overlays.add(
new
ParcelableVenue(c.getLat(), c.getLon(), c.getName(), c.getAddress()));
}
// EMBED INTO INTENT
i.putParcelableArrayListExtra(
"venues"
, overlays);
startActivity(i);
}
});
SharedPreferences sp = getSharedPreferences(Constants.DB, Context.MODE_PRIVATE);
double
lat = (
double
) sp.getFloat(Constants.SP_PREV_LAT, (
float
) Constants.DEFAULT_LAT);
double
lon = (
double
) sp.getFloat(Constants.SP_PREV_LON, (
float
) Constants.DEFAULT_LON);
// GET CLUBS NEAR YOU WITH LAT LON
}
}
The receiving side then looks like:
public
class
VenueMapView
extends
MapActivity {
private
MapView map;
private
List<ParcelableVenue> venues;
@Override
protected
void
onCreate(Bundle icicle) {
super
.onCreate(icicle);
setContentView(R.layout.mapview);
SharedPreferences sp = getSharedPreferences(Constants.DB, Context.MODE_PRIVATE);
double
lat = (
double
) sp.getFloat(Constants.SP_PREV_LAT, (
float
) Constants.DEFAULT_LAT);
double
lon = (
double
) sp.getFloat(Constants.SP_PREV_LON, (
float
) Constants.DEFAULT_LON);
map = (MapView) findViewById(R.id.venue_map);
// INIT MAP
map.setBuiltInZoomControls(
true
);
// SET MAP CONFIGURATIONS
map.getController().setCenter(
new
GeoPoint((
int
) (lat * 1e6), (
int
) (lon * 1e6)));
int
maxZoom = map.getMaxZoomLevel();
map.getController().setZoom(maxZoom -
3
);
Intent i = getIntent();
// RETRIEVE OUR INTENT
venues = i.getParcelableArrayListExtra(
"venues"
);
// GET PARCELABLE VENUES
List<Overlay> mapOverlays = map.getOverlays();
Drawable icon =
this
.getResources().getDrawable(R.drawable.good_pin);
VenueOverlay vo =
new
VenueOverlay(icon,
this
);
// INIT OVERLAY MARKERS
List<OverlayItem> points =
new
ArrayList<OverlayItem>();
for
(ParcelableVenue v : venues) {
OverlayItem o =
new
OverlayItem(v.getGeoPoint(), v.getName(), v.getAddress());
points.add(o);
}
vo.addOverlayList(points);
// ADD VENUE OVERLAYS TO MAP
mapOverlays.add(vo);
}
}
I won’t say too much about what I’m doing with my Maps Activity –
maybe I’ll save this for a future tutorial. The important thing to see
is how I send a list of ParcelableVenue objects, and then retrieve them
on the other side. As you can probably see by now, there’s nothing hard
code wise when implementing a Parcelable object. The difficulty
typically stems from an incomplete understanding of how Android sends
data from process to process – and this is understandable as almost all
of this is abstracted away so that we only need to put simple values
into Bundles and Parcels and voila things magically appear on the other
side. But once you dig a little more into what’s happening behind the
scenes, then all of this serialization and Parcelable implementation
makes much more sense.
And with that I send you off again! Happy coding and hope this helped.
And with that I send you off again! Happy coding and hope this helped.
No comments:
Post a Comment