AnnoPref — make saving SharedPreferences easier

I have been working on Android for years. Whenever making a get/set (or get/put) a field in the SharedPreferences file, I feel hard. It’s, sometimes, because I don’t know which name I should name the preference. Besides, my code would be messed with lots of string constants, and get/put of the SharedPreferences’ object will go along with those constants or I have to define (again) tons of get/set method.

The above reasons encouraged me to learn about Java annotation processor, then, implemented a shortcut way to define preferences’ field name as well as get/set/has functions. Just define a class with fields, AnnoPref will take the rest.

Okay, let’s take a look at this example: if we need to save two preferences for our user settings, one for the startup screen, another for double taps to close. In the days before, I would write like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class UserSettings{
    static final String PREF_STARTUP_SCREEN = "startup-screen";
    static final String PREF_DOUBLE_TAPS_TO_CLOSE = "double-tap-close";
    public static int getStartupScreen(){
        return PreferenceHelper.getInt(PREF_STARTUP_SCREEN, 0);
    }
    public static void setStartupScreen(int screen){
        PreferenceHelper.set(PREF_STARTUP_SCREEN, screen);
    }
    public static boolean isDoubleTapsClose(){
        return PreferenceHelper.getBoolean(PREF_DOUBLE_TAPS_TO_CLOSE, false);
    }
    public static void setDoubleTapsClose(boolean doubleTapsClose){
        PreferenceHelper.set(PREF_DOUBLE_TAPS_TO_CLOSE, doubleTapsClose)
    }
}

This is the version with AnnoPref:

1
2
3
4
5
@Preference
class UserSettings{
    int startupScreen;
    boolean doubleTapsToClose;
}

As you can see, we save 11 lines of code for these two fields. For accessing, just call:

//...
UserSettingsPref.getStartupScreen();
UserSettingsPref.setStartupScreen();
UserSettingsPref.isDoubleTapsClose();
UserSettingsPref.setDoubleTapsClose();
//...

The @Preference class and fields have some more configurations for customization as well as anti hack.

The first @Preference property I would like to mention here is prefix. This String value will apply the prefix for each field key name in the preference file. Although we don’t need to care about the key-value level, this helps us avoid naming same key for two fields in multiple preference classes. Default value of prefix is empty string (annotation doesn’t allow us to use null value).

@Preference(prefix="user-")
class UserSettings{
   int startupScreen;       // key: user-startupScreen
   boolean doubleTapsClose; // key: user-doubleTapsClose
}

If you feel hard in naming a prefix, just apply true to autoPrefix of the annotation @Preference. When turning this value true, the preprocessor will use the package name and class name for the prefix. The priority of autoPrefix is lower than the prefix, therefore, if prefix is not empty, the prefix value will be applied no matter what the autoPrefix is.

package com.tuanchauict.settings

@Preference(autoPrefix=true) //the prefix will be "com.tuanchauict.settings.UserSettings"
class UserSettings{
    ...
}

The third property of @Preference is antiHack. This will hash the key of preference fields into MD5 and Base64. This makes the keys harder to read, not exactly a real way of anti hacking, but it’s worth to give it a try. antiHack is turn off by default.

The last property of @Preference give us a way to define the way we access the preference: SINGLETON or STATIC functions. If we set type to SINGLETON (enum), the instance object of the preference class will be created and the getInstance() as well. The default of type is STATIC.

There are two annotations for fields, one helps us customize the key name of fields, the other give us a way to ignore a field. To customize the field key name, we use @Field(name="custom-key"). And @Ignore for the last feature. When a field name is set, it will be the parameter of the hash function if the antiHack is turned on.

@Preference
class UserSettings{
    @Field(name="startup-screen")
    int startupScreen;
    @Ignore
    String userSection; //userSection won't be generated to get/set functions
}

Currently, the AnnoPref supports the same supported types of SharedPreferences class: boolean, int, long, float, String. Moreover, we can also store list of Integer, Long, Float, String as well as the set of Integer, Long, Float, String. Actually, the list will be serialized to String before being put into preference. The same thing happens to the set of Integer, Long, Float, String.

The document and the code you can grab on GitHub.