/parcelgen

Helpful tool to make data objects easier for Android

Primary LanguagePythonOtherNOASSERTION

Parcelgen

Parcelgen simplifies the process of building objects from JSON descriptions and passing those objects between Android Activities.

What?

Parcelgen is a Python script coupled with a small set of Java utilities (accessible as an Android library project or as a drop-in jar) that generates Java code for basic data objects in an application. Parcelgen generates code for the objects to be created from JSON representations and to read and write thesemselves to Android Parcels.

Included is a complete example application which fetches businesses from the public Yelp API and displays their details, using parcelgen to construct objects from the API response and then to pass the objects between activities.

How?

Describe your data objects in JSON

Create a folder called parcelables in the top level of your Android application. For each object you want to represent with Parcelgen, create a json file with the same filename as your Java class name.

This is the parcelable description for (a subset of) a Business returned by the Yelp API as used in the example app:

{
    "do_json": true,
    "package": "com.yelp.parcelgen",
    "props": {
        "String": [
            "id", "name", "imageUrl", "url", "mobileUrl",
            "phone", "displayPhone", "ratingImageUrl",
            "ratingImageUrlSmall", "snippetText", "snippetImageUrl"
        ],
        "int": ["reviewCount"],
        "double": ["distance"],
        "Location": ["location"]
    },
    "json_map": {
        "ratingImageUrl": "rating_img_url",
        "ratingImageUrlSmall": "rating_img_url_small"
    }
}

The Java files generated by parcelgen can be seen in the example at _Business.java and Business.java. Note that the method toString() in Business.java was added after the code was generated.

In the json description of object you want to create: package is the Java package the object will be placed in, props is a hash of Java type to variable names for instance variables, do_json instructs Parcelgen to generate code to construct the object from JSON, and json_map is for properties that don't map directly to JSON properties; by default Parcelgen will convert Java style CamelCase ivar names to JSON-style under_separated names, but if they don't line up directly json_map can override the default. Parcelgen infers the Java class name from the filename.

Execute parcelgen.py and pass in your description file

$ python ~/parcelgen/parcelgen.py parcelables/Business.json src/

(where src is the path to your Android app's source directory)

Examine your shiny new Java classes

When creating a class for the first time, Parcelgen creates two files, YourClassName.java and _YourClassName.java, in the folder corresponding to the package name specified in the description. YourClassName.java is just small class which inherits from _YourClassName.java and contains a CREATOR property as required by Parcelable.

The magic is in the readFromParcel() and readFromJson() methods parcelgen creates in the superclass. These methods take a Parcel or JSONObject and set the target's instance variables based on the contents of the provided object.

When you re-run parcelgen if you change the properties of on object, parcelgen will overwrite the class beginning with an _ but not the subclass. This allows you to add logic methods and any other custom handling to the underscoreless subclass without losing the ability to modify the object.

Add the parcelgen runtime to your Android project

Parcelgen requires a small Java library with utility methods for reading and writing Parcels and JSON. Download parcelgen.jar here and put it in the libs directory of your Android project (if libs doesn't exist, create it). If you're using Eclipse, you'll also need to add the jar to your build path by right clicking on your project -> Properties -> Java Build Path -> Libraries -> Add Jar -> navigate to your project/libs/parcelgen.jar.

If you'd rather add the library project directly, you can import parcelgen-runtime into Eclipse and add it to your project as an Android library dependency. If you do this, do not add the jar to your project.

Pass the objects around

Want to pass an object to a new activity in an Intent? Just use Intent.putExtra() (BusinessesActivity.java):

intent.putExtra(EXTRA_BUSINESS, mBusinesses.get(position));

Then, in your Activity's onCreate():

Business business = getIntent().getParcelableExtra(EXTRA_BUSINESS);

Want to create a list of objects from a JSON array of dictionaries? Check out how the example does it:

JSONObject response = new JSONObject(result);
List<Business> businesses = JsonUtil.parseJsonList(
    response.getJSONArray("businesses"), Business.CREATOR);

Why?

Since Activities in Android are designed to be independent entities, Android does not allow passing arbitrary objects in memory between them. There are several ways around this. A popular method is to use some sort of global singleton to hold the object(s) you need to pass. As an application grows, this technique gets cumbersome and messy. Additionally, since an Android application can be killed at any time, this doesn't help the activity restart where it was after the its process is killed. Another technique (and one frequently touted by Google's Android team) is to save objects in an sqlite database and pass keys in the database between activities. While this is an excellent technique from an architectual standpoint, the implementation overhead becomes huge as more types of objects are added, and objects' properties change. Finally objects can be seralized using Java Serialization, JSON, or as Android's native high-speed data transport system: Parcels.

While working on Yelp for Android I found myself frequently wanting to pass particular objects or collections of objects (such as Yelp Users or Businesses) between activities in Intents. Java and JSON serialization have a reputation for being particularly slow on Android, so I decided to pass objects to new Activities using Intent.putExtra(String name, Parcelable value). Additionally, when your primary data objects can be written to parcels it's easy to persist them in a Bundle, like for onSaveInstanceState() when your Activity is suspended.

Further Documentation

How parcelgen decides how to persist object properties

Parcelgen supports writing and reading the following types to a Parcel:

  • The native Java types: byte, double, float, int, long, and java.lang.String directly use Parcel's writeType() methods.
  • boolean: Parcels only support arrays of booleans, so Parcelgen will place all of the boolean properties of on object into a boolean array before writing to a Parcel, and unpack them in the same order when reading.
  • java.util.Date: parcelgen stores the value returned by Date.getTime(). When reading from JSON, Parcelgen will convert unix timestamps in seconds since epoch into Java Date objects.
  • Objects which implement Serializable, as specified in the serializables property documented in the next section.
  • Objects which themselves implement Parcelable. If parcelgen doesn't know what do with an object, it assumes the object has a CREATOR property and uses that to write and read the object from a Parcel.
  • Lists or ArrayLists of any of the above object types. Specify the property as a Java generic type: List<Business>, and parcelgen will use the above logic to read and write the contents of the list.
  • Arrays of both primitive and complex types are also supported

Properties you can use in a parcelgen json description

  • props: A dictionary of Java type name to list of instance variable names.
  • package: The Java package where the generated class should be placed
  • imports: Additional classes to import, in case a member's type is not in the same package as the object being described. Note that parcelgen handles importing the correct classes for any types officialy supported (such as Date and Uri).
  • serializables: A list of any properties which should be read and written using Serializable. Since parcelgen doesn't inspect the Java files, it can't tell whether a class is Serializable or Parcelable.
  • do_json: Whether to generate the code to read this class from json. Leave this out or set it to false if you won't be using parcelgen's json reading/writing features.
  • json_map: A dictionary of instance variable to json property for properties that parcelgen cannot guess, such as {"dateCreated": "time_created"}.
  • json_blacklist: A list of properties which shouldn't be read from json. Use this for properties you want to passed in Parcels but not read from json.
  • do_json_writer: If enabled, Parcelgen will generate code to create a JSONObject mirroring what would have been read from JSON. Note that this is currently experimental, in particular, List-type properties are not supported.
  • default_values: An optional dictionary containing the value certain properties should be given by default. For instance useful to defaulting integer values to -1:
"default_values": {
    "geoAccuracy": -1
}
  • make_serializable: Boolean indicating whether or not to mark the generated class(es) as implementing Serializable.
  • transient: List of fields to be marked transient (you'll probably also want to declare make_serializable):
"transient": {
    "accessTime",
    "distanceCache"
}

Missing Features and Further Work

Parcelgen does not integrate with any of Android's build tools or Eclipse. I don't know ant, but it should be possible to have parcelgen automatically update generated files when its json descriptions are changed, in which case the classes generated by parcelgen shouldn't be kept in version control

Parcelgen only reads and writes json using the built-in json.org based JSONObject interface. Support for other parsers such as Jackson would be a great addition.