John Petitto About Blog

Typedef Annotations in Android

Java’s enum type is the standard way for defining a set of related constants. For instance, we can define an enum to represent different units of temperature:

public enum TemperatureUnit {
    CELSIUS, FAHRENHEIT, KELVIN
}

The nice thing about an enum is that it’s type-safe. Wherever a TemperatureUnit value is expected, only an enum value may be used:

public void setConversionUnit(TemperatureUnit unit) { ... }

setConversionUnit(TemperatureUnit.CELSIUS);
setConversionUnit("Celsius"); // not allowed

Enums possess even more power since they can contain both methods and constructors as part of their definition. This allows them to behave similar to class types.

Before the enum type was introduced in Java 5, programmers would typically define a set of integer constants instead. Using our previous example of temperature units, we might define the constants as follows:

public class TemperatureConverter {
    public final static int CELSIUS = 1;
    public final static int FAHRENHEIT = 2;
    public final static int KELVIN = 3;
    ...
}

You’ll often find integer constants used in favor of an enum in the Android APIs. For example, the ViewGroup.LayoutParams class defines the constants FILL_PARENT, MATCH_PARENT and WRAP_CONTENT. The reason for this is related to performance. When compared to integer constants, enums may use more than twice the amount of memory. Since many Android devices are resource constrained, memory footprint is a legitimate concern.

The main drawback of using integer constants though is the lack of type safety. We no longer have the compiler enforcing a value to be from our set of constants. If the setConversionUnit method is modified to accept an argument of type int, then any integer value can be passed in:

public void setConversionUnit(int temperatureUnit) { ... }

setConversionUnit(CELSIUS);
setConversionUnit(34); // also allowed, but not an expected value

The Android team at Google recently introduced a support library for annotations. Among the many included annotations is the new @IntDef annotation. Its purpose is to provide type safety for traditional integer constants:

@IntDef({CELSIUS, FAHRENHEIT, KELVIN})
@Retention(RetentionPolicy.SOURCE)
public @interface TemperatureUnit {}

// same as before

public final static int CELSIUS = 1;
public final static int FAHRENHEIT = 2;
public final static int KELVIN = 3;

We define an annotation named TemperatureUnit, acting as a namespace for our set of constants. We apply the @Retention annotation to its definition with a value of RetentionPolicy.SOURCE since we only need it to exist at compile time. Finally, we add the @IntDef annotation with the names of our constants.

Now we can annotate our code with @TemperatureUnit and get the added type safety:

public void setConversionUnit(@TemperatureUnit int temperatureUnit) { ... }

setConversionUnit(CELSIUS);
setConversionUnit(34); // no longer allowed

The compiler will generate a warning in the case that one of expected constant values is not provided. While not as forceful as a compile-time error, it can be helpful in notifying both developers and continuous integration servers of type safety issues.

Sometimes it makes sense to apply more than one constant value at a time. For this, the @IntDef annotation provides a flag attribute, allowing multiple constant values to be combined using bitwise operators:

@IntDef(flag=true, values={
    BOLD, ITALIC, UNDERLINE
})
@Retention(RetentionPolicy.SOURCE)
public @interface TextStyle {}

public final static int BOLD = 1;
public final static int ITALIC = 2;
public final static int UNDERLINE = 3;

public void setTextStyle(@TextStyle int style) { ... }

setTextStyle(BOLD|ITALIC);

Now multiple constant values can be used simultaneously. When a value of combined constants is passed, the pattern is type-checked by the @IntDef annotation. An alternative is to use the EnumSet class from the Java APIs. The issue again is the memory consumption when compared to the more lightweight integer constants.

The other typedef annotation that the support library provides is @StringDef. While having integer constants is sufficient in most cases, treating constant values as strings can be useful. We recently made use of this in CafeJava, which provides RxJava extensions to the MobileFirst Platform API. The API allows the programmer to specify an HTTP method for a WLResourceRequest. It expects a String and within that class is a set of String constants representing the different HTTP methods. CafeJava takes advantage of the @StringDef annotation to ensure a valid constant value is passed in:

@StringDef({GET, POST, PUT, DELETE})
@Retention(RetentionPolicy.SOURCE)
public @interface HttpMethod {}

public static final String GET = WLResourceRequest.GET;
public static final String POST = WLResourceRequest.POST;
public static final String PUT = WLResourceRequest.PUT;
public static final String DELETE = WLResoureRequest.DELETE;

Thanks to these new annotations, we now have a way to specify sets of constants that are both performant and type-safe. You can start using them in your own code by adding the following gradle dependency:

compile 'com.android.support:support-annotations:22.1.1'