Android Dev Tips: How to replace Enum by int or String efficiently?

AndroidAndroidAnnotationsEnumTipsAndTricks

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.

example of error messages

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;
                }
            }
        }
    }
}