As a new member of Infinum's Android team, I asked a lot of questions about how they work, what standards they use - in general, how they write their apps. I soon noticed how often my new teammates use enums for declaring types and states, and asked what they thought about Android magic constants. This post is the result of a discussion we had had and the presentation that was held after that.

What are magic constants?

Magic constants are regular old Java constants decorated in a way that allows tools to recognize them as special values. Their syntax is not different from any other Java constant. Instead, an annotation with an easily readable name which lists all related constants has to be created. This annotation can then decorate a return value or a method parameter, giving hints to tools you use about the values that are accepted or can be expected. Their main purpose is to enforce type safety for method parameters and/or method return values while writing the source code.

The idea comes from JetBrains' @MagicConstant annotation which, for the most part, does the same thing. The only difference is that support-annotations, the package that gives us Android magic constants, holds a lot more than just magic constants.

How to use them?

A very simple and excellent example is available in the official Android docs. For the sake of originality, I'll take an example from what I've written recently.

public class PermissionDescriptor { public static final int GRANT_POLICY_NEVER = 0 ; public static final int GRANT_POLICY_IMMEDIATELY = - 1 ; public static final int GRANT_POLICY_ON_REQUEST = - 2 ; public static final int GRANT_POLICY_ON_SECOND_REQUEST = - 3 ; public final String permission ; public final int grantPolicy ; public int requestCount = 0 ; public PermissionDescriptor ( String permission , int grantPolicy ) { this . permission = permission ; this . grantPolicy = grantPolicy ; } }

PermissionDescriptor is a POJO class that describes a single Android permission in a testing environment. String permission should be one of the permissions available in the Manifest.permission class, while int grantPolicy should be one of the constants defined in the PermissionDescriptor class.

Unfortunately, there is no guarantee that the user will use the constructor with proper values - there is no type safety here. This means that one could easily initialize an object by providing bogus data:

PermissionDescriptor invalidDescriptor = new PermissionDescriptor ( "" , 5 );

Although technically correct, the constructor is not used properly and the object created with it may not yield any valid results. That's where magic constants from support-annotations come in handy.

@Retention ( RetentionPolicy . SOURCE ) @IntDef ({ GRANT_POLICY_NEVER , GRANT_POLICY_IMMEDIATELY , GRANT_POLICY_ON_REQUEST , GRANT_POLICY_ON_SECOND_REQUEST }) public @interface GrantPolicyDef { }

@Retention is a built-in annotation in Java that describes when the annotation will be discarded. You can read more about this here.

All values that should be possible as grantPolicy parameters are listed in an @IntDef annotation. This is a very simple example where a Java annotation interface is created and annotated with an @IntDef annotation listing all possible values.

It doesn't need to have Def as a name suffix, but I like to follow the rule of using <Type name>Def as a naming convention.

The previous constructor can now be easily decorated with a newly created annotation.

public PermissionDescriptor ( @PermissionDef String permission , @GrantPolicyDef int grantPolicy ) { this . permission = permission ; this . grantPolicy = grantPolicy ; }

The same constructor call from the last example now shows an error in Android Studio, simply because now it knows what values to expect. Moving the cursor over the problematic part of the code displays what is expected for that parameter.

This won't necessarily stop you from compiling and running an app as this check is done just on the tooling side. However, you are able to run a lint command from the terminal and enforce these checks there, making them accessible for continuous integration.

As you can see, I am using a @PermissionDef annotation. That is a @StringDef annotation that works just like @IntDef , but with Strings instead of ints. Unfortunately, as opposed to magic constants created by JetBrains, there is no simple way to list all constant fields created in one class. Instead, I've created a @PermissionDef annotation listing all available fields from the Manifest.permission class and it is available in the following gist.

Hasn't something similar already been possible?

Magic constants are a response to the ever-growing number of APIs in the Android framework, where the state, type of a component, or an action are chosen by providing an int or String constant. For example, setting view visibility with int constants or requiring a system service with String constants.

// setting view visibility view . setVisibility ( View . VISIBLE ) // actual value is int 0x00000000 // getting notification service context . getSystemService ( NOTIFICATION_SERVICE ); // actual value is String "notification"

These values could've easily been specified as enumerated types that have existed in Java for quite some time - enums. The compiler would then do its job with type checking and throw a compiler error if you provided an invalid type. No need for additional checks from the Android Studio.

The code can even look more elegant than it does with constants since you don't have to define any arbitrary values - only the constant name is required.

public class PermissionDescriptor { /** * Only these values can be set * as a GrantPolicy to PermissionDescriptor. */ public enum GrantPolicy { GRANT_POLICY_NEVER , GRANT_POLICY_IMMEDIATELY , GRANT_POLICY_ON_REQUEST , GRANT_POLICY_ON_SECOND_REQUEST } public final String permission ; public final GrantPolicy grantPolicy ; public int requestCount = 0 ; public PermissionDescriptor ( String permission , GrantPolicy grantPolicy ) { this . permission = permission ; this . grantPolicy = grantPolicy ; } }

The rewritten PermissionDescriptor example now looks slightly different with a newly defined type called GrantPolicy .

The constructor parameter GrantPolicy grantPolicy does not need any decorator - the compiler will complain if a wrong value is provided to the constructor.

This raises an interesting question that is still being discussed today.

Why aren't enums the standard?

The correct question may be Why aren't enums used more widely in the Android SDK framework?

The reasons are purely performance-driven.

When compiled, enums turn into a POJO class which has all enumerated values as fields of the enum type. You can easily check this by running the javap tool on the .class file from the terminal.

public final class package . PermissionDescriptorEnumerated $GrantPolicy extends java . lang . Enum < package . PermissionDescriptorEnumerated $GrantPolicy > { public static final package . PermissionDescriptorEnumerated $GrantPolicy GRANT_POLICY_NEVER ; public static final package . PermissionDescriptorEnumerated $GrantPolicy GRANT_POLICY_IMMEDIATELY ; public static final package . PermissionDescriptorEnumerated $GrantPolicy GRANT_POLICY_ON_REQUEST ; public static final package . PermissionDescriptorEnumerated $GrantPolicy GRANT_POLICY_ON_SECOND_REQUEST ; public static package . PermissionDescriptorEnumerated $GrantPolicy [] values (); public static package . PermissionDescriptorEnumerated $GrantPolicy valueOf ( java . lang . String ); static {}; }

Running the javap tool on GrantPolicy.class yields these results.

Each field is instantiated as a GrantPolicy type. Since those are just objects in Java, we know that each declaration will take some runtime memory simply to reference the object, and more memory for allocating all the data each object carries (name and ordinal for plain enums).

In the GrantPolicy example, that's about 70 bytes for each field. That, granted, isn't a lot, but compared to a simple int constant, which takes up only 4 bytes, it's a big difference. Imagine having hundreds of enums - which Android could have if all String and int constants were not used. Runtime allocation size would be much bigger.

This effect is much more prevalent when used in libraries that apps depend on, or in the apps themselves, where memory is a precious resource.

Having enums in your app is also not free - they require a new class (as presented with a javap command), which in turn generates more bytecode. In Android, all application bytecode is loaded into the memory. More bytecode means that more memory has to be used and initial loading times may be longer.

Memory taken up by bytecode does not directly contest with the memory used in runtime, but it contributes to the global memory allocation, meaning that the OS may decide to remove the app from the memory once there is no more space on the device.

Below is a very generous comparison of enums versus magic constants memory allocation and APK size increase, both in bytes.

Enums add at least two times more bytes to the total APK size than plain constants and can use 5 to 10 times more RAM memory than equivalent constants.

We developers also rarely develop exclusively for modern phones - older and slower devices with much more limited memory are still used by a sizable chunk of users. Optimizing for the lowest denominator should be a priority while still trying to perform better on higher-spec devices.

Samsung Galaxy S3 Mini is still very popular, but it is considered slow and limited for today's standards. Optimizing as much as possible for such devices should be a priority.

This is actually the source of the issue and probably the main reason behind @IntDef and @StringDef in support-annotations library.

Colt McAnlis did the math and summarized a lot related to this topic in his amazing video.

I personally recommend watching all of his videos and videos in the Performance Patterns series.

For the sake of comparison, the code block below is the result of running javap on the GrantPolicyDef annotation.

public interface package . PermissionDescriptor $GrantPolicyDef extends java . lang . annotation . Annotation { }

It generates a new class just like an enum does, but it's empty. As it was already mentioned, it is used only with the tools and while writing the source code which results in this minimalist footprint during runtime. Due to all these reasons, Google strongly recommends not using enums.

Does it even matter?

This is just a recommendation for developers, a best practice advice. Even so, as I said earlier and as Colt mentioned in his video, the discussion surrounding this topic is still very heated.

Premature optimization is an argument often used when stating that there are far more important issues in Android that we should be caring about. While that's true, I'd like to make a point that every little bit counts. While code readability is important, it is much more important to ensure that the application behaves smoothly for each user who might use it. Even if that means sacrificing some of the habits we all love.

For a conclusion, I'll just quote what Colt said on Jake Wharton's post about the same topic:

Please stop thinking of things like Enums/ArrayMaps/Iterator fixes as Premature Optimisations. That's really the wrong mentality. This is "Preventative Optimizations". Memory is a scarce resource on Android; it will run out.

So yes, PLEASE : optimize for your bitmap sizes; Reduce allocation churn; re-use bitmap space; Pick a smaller serializations format; use WebP; all the big stuff you should be doing. But don't neglect the small stuff. The small preventative work that you do in your code to save memory is saving you big in the long run.

It's not premature. It's preventative.