The Android Bundle is a convenient and sometimes necessary object for wrapping data to pass across process boundaries and send with intents, as well as to store state between configuration changes. Unfortunately, wrapping data in a Bundle often requires repetitive and error-prone code. What makes this even worse is that a Bundle attached to an intent from GCM (Google Cloud Messaging) loses type information because GCM serializes the data as String-String key-value pairs.

When investigating a solution for how to eliminate the unbundling of Bundled GCM objects for Uber’s Android rider app, we realized that not only is the data provided generally read-only, but also used to populate data shown on a notification and determine business logic for corresponding pending intents. These objects are prime examples of value classes; classes which are final, immutable, and have implementations of equals, hashCode, and toString based only on the instance’s state.  

Generating value classes is simple using AutoValue, which takes an abstract class that defines its fields by its getter method return type and names and then generates a concrete value class that subclasses the interface. AutoValue also provides a framework for extensions by chaining them together. Below, we provide an example of the AutoValue framework for chaining together extensions:

final class AutoValue_BundledObject extends $AutoValue_BundledObject {
   // Unbundle implementation
}

final class $AutoValue_BundledObject extends BundledObject {
   // AutoValue value type implementation
}

@AutoValue
public abstract class BundledObject {
   static BundledObject create(…) {
       return new AutoValue_BundledObject(…);
   }
   // Abstract properties
}

While an AutoValue extension exists to handle the repetitive Parcelable code, there was not one for Bundles. To address this at Uber, we created and open sourced AutoValue: Bundle Extension. In this article, we discuss how our new tool unbundles data and handles type interferences, enabling Android engineers to quickly unbundle data into a value class. This tool decreases the likelihood of encountering bugs and iterative code as well as losing type information, in turn improving the overall app experience for our Android users.

 

Unbundling data

Our AutoValue: Bundle Extension will run if the abstract base class is annotated with @AutoValue and there is a public static method that returns the class type with two parameters: the bundle and a Gson instance. When it runs, the extension generates a typical value class but adds a static unbundle method which takes the bundle and Gson instance as parameters and returns an instance of the annotated class, as shown in the example below:

@AutoValue
public abstract class Foo {

   public static Foo create(Bundle bundle, Gson gson) {
       return AutoValue_Foo.unbundle(bundle, gson);
   }

   public abstract String bar();
}

As displayed above, AutoValue generally calls its subclass’ constructor to create a new instance by passing in values for all fields. However, we use a static method because we only want to take in two parameters, the bundle and the deserializer, Gson. The unbundle method will properly handle calling the proper get methods on the bundle.

In the example above it will call bundle.getString(“bar”). Similar to standard AutoValue classes, the return type and the parameter name are used; however, here they determine which method we call on the bundle and what String we pass as a parameter. Method names should be written in lower camel case (fooBar) and will be converted to parameter names in snake case (foo_bar). While this is a rigid limitation (see ‘Next steps’ for more on this topic), it is important to remember when naming your parameters, especially if the data is provided from an external source.

 

GCM and type inference

The simple version above handles the case where the data is stored in the bundle as its proper type; however, as mentioned before, GCM serializes all data in String-String pairs. To handle this, the extension includes a class-level annotation @GCMBundle which reads all data from the bundle as a String and then uses primitive type parsing or the Gson instance to convert to the proper return type. It can even handle parameterized objects such as an ArrayList<Foo>.

The Gson instance will be used to unbundle other object types by getting the object from the bundle as a String and using a TypeToken to deserialize it. Additionally, the generated class also contains several overloaded helper methods, “toPrimitive” for converting Byte, Short, Char, and Float arrays to their primitive array corollaries. This is because Gson cannot read primitive type arrays from a Json String without a custom deserializer. For example, if a Byte array is stored in a GCM Json string the extension would generate:

public static Test unbundle(Bundle bundle, Gson gson) {
  return new AutoValue_Test(toPrimitive(gson.fromJson(bundle.getString(“byte_array”), Byte[].class))
}

public static byte[] toPrimitive(Byte[] byteParam) {
   byte[] bytes = new byte[byteParam.length];
   for (int i = 0; i < byteParam.length; i++) {
       bytes[i] = byteParam[i];
   }
   return bytes;
}

 

Next steps

As we continue to grow the capabilities of the AutoValue: Bundle Extension, possible additions include:  

  • New deserializers: Currently, the extension supports deserialization using only Gson; however, it was built to support multiple deserializer classes, as well. To do so, we wrap the deserialization of objects behind a Deserializer interface which generates the deserializer-specific code. We plan to add support for new deserializers, such as Moshi, by updating the applicable method in the extension class and creating a new Deserializer class which writes the specific code.  
  • Parameter case specification: Adding support for parameter case specification would allow for greater developer flexibility. One possible method for accomplishing this would be to use Guava’s CaseFormat as a parameter to the annotation.

AutoValue: Bundle Extension is a solution we built to generate code that would otherwise be time-consuming and error-prone. Add it to your project by simply including AutoValue and the extension as annotation processors in your dependencies, and if you want to use the @GCMBundle, also include the extension as provided.

To learn more about Uber Engineering’s open source efforts, check out our Github page!

Zachary Sweigart is an Android engineer on Uber’s Shared Rides team.

Photo Header Credit: “Neon gobi exiting coral head” by Conor Myhrvold, Bonaire, ABC Islands.

0 Comments