Tuesday, January 25, 2011

Pass me the Data

You've written the Hello Android app, then created your own simple app. Now you've created a much more complicated app with several Activities. You may need to be able to pass data between your Activities. In this post, I will take you from the basics of passing simple data types between Activities right up to making your own custom objects Parcelable so they can also be passed along with the same ease as a simple data type.

Before we start passing data around, first you will need to understand how an Activity is launched. If you're reading this, then you are probably familiar with Intents. If not, then you should not pass go, and definitely not collect $200. Instead go here and learn about Android Application Fundamentals.

To launch a new activity, you would create an Intent and pass it to the Activity method startActivity().

Intent newIntent = new Intent(Context, Class);
startActivity(newIntent);

Now lets say you have a ListActivity and whenever a user taps on an entry in the list, you want to launch a new Activity that does something with the data from that list entry. The most basic scenario for passing data is to attach an extra to the Intent in the form of

newIntent.putExtra(name,value);

Where name is a String that is used to "tag" your data and value is the actual data object you are passing to the intended Activity. Value can be many different types, including String, int, Boolean, etc. You can see the complete list of putExtra methods here.

Here's an example from the calling Activity.

Intent newIntent = new Intent(this, SomeActivity.class);
newIntent.putExtra("MAGIC_NUMBER", 42);
startActivity(newIntent);

Now on the called Activity side (receiving the Intent), in your Activities onCreate() method, you will need to retrieve the extra data from the Intent. You can do this by calling getIntent() to get the Intent that started the Activity, then getIntExtra(name, default). This is if you passed an int value, if you passed a different type, you would use the corresponding method for that type. For example..

int magic_number = getIntent().getIntExtra("MAGIC_NUMBER", 420);

Note that the default value is used if the tag "MAGIC_NUMBER" had no value assigned to it.

This is great if you are just passing one piece of data using a basic data type, but if you are passing more data than just a single type, you might want to consider using Bundle.  Basically a Bundle is just a mapping of tag-data pairs grouped together into one passable object.

Lets say you had a contact list and when the user taps a contact, you want to pass the name, id and phone number to the called Activity.  You could do so like this.

Intent newIntent = Intent(this, ContactDetails.class);
Bundle extras = new Bundle();
extras.putString("CONTACT_NAME", name);
extras.putInt("CONTACT_ID", id);
extras.putString("CONTACT_NUMBER", number);
newIntent.putExtras(extras);
startActivity(newIntent);

And on the receiving Activity...

Bundle extras = getIntent().getExtras();
String name = extras.getString("CONTACT_NAME");
int id = extras.getInt("CONTACT_ID");
String number = extras.getString("CONTACT_NUMBER");

Now lets say you've created your own data class with several different types of data representing the class.

public class Dog extends Object {
  private String mType;
  private String mName;
  private int mId;
 
  public Dog(String type, String name, int id) {
    mType = type;
    mName = name;
    mId = id;
  } 

You could create an instance of Dog and pass each piece of data separately by adding 4 tag-data pairs to a Bundle and passing the Bundle, but a smarter way of doing this is to implement the Parcelable interface in your Dog class.  The Parcelable interface has 2 methods you need to implement, describeContents() and writeToParcel(Parcel, int).


    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(mType);
        dest.writeString(mName);
        dest.writeInt(id);
    }

In this method you must add each of your data types to the Parcel with the correct write method for it's data type.

If your class will have child classes, you can use the describeContents() method to differentiate between child classes so that when you unparcel your parceled object, you can create the correct child object.

This is only half of the solution.  Now that you have the facilities to write your object to a Parcel, you also need to be able to rebuild your object in the called Activity.  First you will need a new constructor for your class.  One that takes a Parcel object and can populate it's data members from it.


    public Dog(Parcel dest) {
        mType = dest.readString();
        mName = dest.readString();
        mId = dest.readInt();
    }

Note the order.  You must read your data types from your Parcel in the same order you wrote them.  Essentially you're flattening your data into one data stream, then reading it back on the other side.

Finally, you will need to create a Parcelable.Creator in your class that will trigger your new Parcelable constructor when needed.


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

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

One last thing I'll leave you with is the concept of chaining your Parcelable objects.  Lets say your Dog class has a custom object as one of it's data members.  You will want to make sure that class also implements the Parcelable interface.  In order to support parcelizing your custom object inside your Dog class, you will need to change 2 methods.

First Dog's writeToParcel(Parcel, int) method needs to tell your custom object to write itself to the parcel.  To do this we can call writeParcelable(Object, int).  This method will invoke your custom classes writeToParcel method.

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(mType);
        dest.writeString(mName);
        dest.writeInt(id);
        dest.writeParcelable(dogTag, flags);
    }

Here dogTag would be your custom class that could implement say, a phone number and address for where the dog lives.

Second Dog's Parcel constructor.  Here we will call readParcelable(Classloader) to get the custom objects parceled data.

    public Dog(Parcel dest) {
        mType = dest.readString();
        mName = dest.readString();
        mId = dest.readInt();
        dogTag = dest.readParcelable(DogTag.class.getClassLoader());
    }

This should get you passing your data around to your activities!