Android Dev Tips: How to replace Enum by int or String efficiently?
There’s a good topic on developer.android.com about how to manage app’s memory. I want to touch specific paragraph about enums:
Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.
Lot of developers don’t want to follow this rule, saying this is inconvenient, hard to navigate through code, hard to debug, blah-blah-blah…
Android guys want to help developers with this. They provided android.support.annotation package with lot of powerful annotations for Android Studio and Lint. I want to explain more about IntDef and StringDef. There two classes may help to replace enums efficiently for both: performance and convenience.
Let’s look at two typical enum classes:
enum Status {
IDLE,
PROCESSING,
DONE,
CANCELLED,
}
enum Category {
NONE,
NORMAL,
HIDDEN,
}
Nothing special, short set of logically named constants. If we replace them by static final variables, then we’ll automatically loose completeness of these declarations. It will be set of unlinked constants. Here is good tutorial how to use android annotations. It says:
The @IntDef annotation lets you basically create a “typedef”, where you create another annotation which represents the valid integer constants that you expect, and then you decorate your API with this typedef annotation.
It’s okay, good idea. But what if we’ll go even further? We will place our constants directly in the annotation class, like this:
@IntDef({Status.IDLE, Status.PROCESSING, Status.DONE, Status.CANCELLED})
@Retention(RetentionPolicy.SOURCE)
@interface Status {
int IDLE = 0;
int PROCESSING = 1;
int DONE = 2;
int CANCELLED = 3;
}
@StringDef({Category.NONE, Category.NORMAL, Category.HIDDEN})
@Retention(RetentionPolicy.SOURCE)
@interface Category {
String NONE = "none";
String NORMAL = "normal";
String HIDDEN = "hidden";
}
It looks a bit more complicated then enum declaration, but let’s check how it works in code. Here is the simplest class declaration with two fields representing our enums. All we need to do is just mark both: fields and accessors with the annotations.
class Foo {
@Status
int status;
@Category
String category;
@Status
int getStatus() {
return status;
}
void setStatus(@Status int status) {
this.status = status;
}
@Category
String getCategory() {
return category;
}
void setCategory(@Category String category) {
this.category = category;
}
}
And now let’s try to assign some values out of declared constants set. You can see error messages directly in editor.
It also works for switch-statement.
void bar(@Status int status, @Category String category) {
switch (status) {
// TODO the same warning is here
case 42: break;
// TODO correct usage
case Status.CANCELLED: break;
case Status.DONE: break;
}
if (category != null) {
switch (category) {
// TODO unfortunately here is no warning; I'll create issue for Android guys :)
case "any": break;
// TODO correct usage
case Category.NONE: break;
case Category.HIDDEN: break;
}
}
}
Thus we can replace enum classes by simple constants without losing comfort of using strictly declared types. Beside that we improve performance of our apps.
Full source code of this example is at the end of this post.
Source code
IntAndStringInsteadOfEnum.java
import android.support.annotation.IntDef;
import android.support.annotation.StringDef;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
class Before {
enum Status {
IDLE,
PROCESSING,
DONE,
CANCELLED,
}
enum Category {
NONE,
NORMAL,
HIDDEN,
}
}
class After {
@IntDef({Status.IDLE, Status.PROCESSING, Status.DONE, Status.CANCELLED})
@Retention(RetentionPolicy.SOURCE)
@interface Status {
int IDLE = 0;
int PROCESSING = 1;
int DONE = 2;
int CANCELLED = 3;
}
@StringDef({Category.NONE, Category.NORMAL, Category.HIDDEN})
@Retention(RetentionPolicy.SOURCE)
@interface Category {
String NONE = "none";
String NORMAL = "normal";
String HIDDEN = "hidden";
}
class Foo {
@Status
int status;
@Category
String category;
@Status
int getStatus() {
return status;
}
void setStatus(@Status int status) {
this.status = status;
}
@Category
String getCategory() {
return category;
}
void setCategory(@Category String category) {
this.category = category;
}
void foo() {
// TODO wrong field assignment
status = 42;
category = "hello";
// TODO correct field assignment
status = Status.IDLE;
category = Category.NORMAL;
// TODO wrong using of setters
setStatus(142);
setCategory("world");
// TODO correct using of setters
setStatus(Status.PROCESSING);
setCategory(Category.HIDDEN);
// TODO wrong arguments passing
bar(242, "android");
// TODO correct arguments passing
bar(getStatus(), getCategory());
bar(Status.DONE, Category.NONE);
}
void bar(@Status int status, @Category String category) {
switch (status) {
// TODO the same warning is here
case 42: break;
// TODO correct usage
case Status.CANCELLED: break;
case Status.DONE: break;
}
if (category != null) {
switch (category) {
// TODO unfortunately here is no warning; I'll create issue for Android guys :)
case "any": break;
// TODO correct usage
case Category.NONE: break;
case Category.HIDDEN: break;
}
}
}
}
}
