View on GitHub

Esperandro

Easy SharedPreference Engine foR ANDROid

Download this project as a .zip file Download this project as a tar.gz file

esperandro

Easy SharedPreference Engine foR ANDROid

What Is It?

esperandro helps Android developers using the SharedPreferences of the Android system. Normally there is a lot of boilerplate code when using them. One has to know the name and the type of a specific preference everywhere this preference is used.

With esperandro you define once which keys exist, along with their type. The usage of these preferences is now both, typesafe and easy.
Esperandro uses compile-time code generation to generate reusable boilerplate code for you.

Usage

To work with esperandro we have to include the esperandro-api in our Android App. The esperandro annotation processor itself has to be in the classpath of the java compiler for annotation processing. This is done with some configuration in the build.gradle file. This is shown below in the chapter "configuration".

Defining Preferences

When defining the preferences used by our App we start with an emtpy interface and annotate it with @SharedPreferences. At this point we have two possibilities, either we don't specify any parameters at the annotation and end up with the default preferences, or we give them a name (and optionally a mode) to access a SharedPreference with the given name and mode.

Something like

@SharedPreferences
public interface DefaultPreferences {

would mimic a call as PreferenceManager.getDefaultSharedPreferences(context).

In contrast a definition like

@SharedPreferences(name = "eventPrefs", mode = SharedPreferenceMode.PRIVATE)
public interface EventPreferences {

would mimic a call as context.getSharedPreferences("eventPrefs", Context.MODE_PRIVATE).
The mode given here equivalents to the possible Context.MODE_* attributes. If no mode is given explicitly, SharedPreferenceMode.PRIVATE is assumed.

Getters and Setters for Preference Values

For each preference value of a given name there should be two methods in the interface. One getter and one setter. A getter returns the desired type of the preference. It's method name is the name that is used to retrieve the value from the SharedPreference object. It takes no further parameters.
A setter returns void and takes exactly one parameter. This parameter but be the type that should be stored in the preference. Again the name of the method is the key used to access the underlying Android SharedPreference object. The naming of the methods does not use prefixes like "get", "set" or "is".

For each value that should be stored in a specific SharedPreference we end up defining a pair of methods like:

String nameOfPreference();
void nameOfPreference(String nameOfPreference);

The possible types are int, long, float, boolean, String and Set<String>.

As an alternative there is support for setters with a boolean return value. Esperandro will then return information about the success of the commit operation like documented in the official docs.

This looks like so:

boolean nameOfPreference(); // boolean getter
boolean nameOfPreference(boolean nameOfPreference); // boolean setter with information about success

API Compatibility

By default, Esparandro will use apply() when changing a preference value. This method was added in API Level 9 (Gingerbread). If your app requires compatibility with earlier APIs, make sure your setters return a boolean as described above. This way you can guarantee that commit() is used.

POJO preferences

Sometimes it can be useful to not only be able to store primitive or String preferences but also POJOs.
Esperandro provides an API for this use-case, too. There exists a Serializer interface that cat be implemented to be able to serialize and deserialize objects into a String preference.

A Serializer can be set by calling Esperandro.setSerializer(Serializer serializer). After this call all objects that are not compatible with Android's SharedPreference structure will be stored into a String preference using the previously set Serializer.

If you don't want to provide your own Serializer you can use the esperandro gson addon that will automatically be used to serialize objects if no other Serializer was set. This addon will serialize objects into a JSON string.

If you are using Jackson in your project for JSON-Serialization there is the esperandro jackson addon that can be used.

After setting your Serializer or adding one of the addons to the classpath you can use the following definition for object preferences:

Container nameOfPojoPreference();
void nameOfPojoPreference(Container nameOfPojoPreference);
where Container ca be any class that can be serialized by your Serializer implementation.

Defaults

With no further configuration the following defaults apply when retrieving preference values that were not set before:
int - -1
long - -1l
float - -1.0f
boolean - false
String - "" (empty String)
Set<String> - null

For all types except Set<String> other defaults can be given using the @Default annotation. To make use of this mechanism the getter of a preference value has to be annotated with this annotation. The desired default can then be configured using the respective of* property of the annotation where * expands to the type of the getter.

You then finally end up with something like

@Default(ofString = "superFancyDefaultValue")
String nameOfPreference();
void nameOfPreference(String nameOfPreference);
in your interface.

Runtime Defaults

In addtition to the annotated compile-time defaults, it is possible to provide runtime-defaults. To use them the getter method for a preference has to be specifically defined. Every getter that allows a runtime-default must not have the @Default annotation but instead end with $Default in the method name. The one and only parameter has to be of the type of the preference and resembles the given value that should be used as default.

Example:

String nameOfPreference(); // getter with implicit compile-time-default

String nameOfPreference$Default(String defaultValue); // getter with runtime-default

void nameOfPreference(String superFancyPreferenceValue); // putter

Obtain Instance

Obtaining an instance of the generated class is easy. You just call

Esperandro.getPreferences(Class<T> preferenceClass, Context context)

where preferenceClass is the Class of your interface.

Advanced Features

Additional actions

All generated classes do not only implement your defined preference interface but also the de.devland.esperandro.SharedPreferenceActions interface. That means, if you let your interface extend it you have access to these convenience methods:

    /**
     * @return the underlying SharedPreference instance.
     */
    SharedPreferences get();

    /**
     * Checks if a value for the given key exists.
     * @param key
     * @return true if the given key exists, false otherwise
     */
    boolean contains(String key);
	
    /**
     * Removes the value for the given key.
     * @param key
     */
    void remove(String key);

    /**
     * Registers a callback to be invoked when a change happens to a preference.
     * @param listener The callback that will run.
     */
    void registerOnChangeListener(
            SharedPreferences.OnSharedPreferenceChangeListener listener);

    /**
     * Unregisters a previous callback.
     * @param listener The callback that should be unregistered.
     */
    void unregisterOnChangeListener(
            SharedPreferences.OnSharedPreferenceChangeListener listener);

    /**
     * Clears the complete sharedPreferences of the previously given name. (Be aware that ALL preferences under this
     * name are cleared not only the ones defined in your interface)
     */
    void clear();

    /**
     * Initializes the underlying SharedPreference object with the respective explicit or implicit default values. This
     * can be useful when the defaults should be shown in the summary in a PreferenceActivity.
     */
    void initDefaults();

Inheritance

If the annotated interface extends from other interfaces that define getters and putters for preferences, the generated class will implement them too. This way you can order the preferences hierarchically or even use common preference keys in different SharedPreference files.

Proguard

# esperandro
-keepnames class de.devland.** { *; }
-keep class **$$Impl { public *;}

# keep the annotated things annotated
-keepattributes *Annotation*, EnclosingMethod, Signature, InnerClasses

# for dagger also preserve the interfaces
# assuming they reside in the sub-package 'preferences' and all end with 'Prefs'
#-keep class preferences.**Prefs { public *;}

# jackson
#-dontwarn com.fasterxml.jackson.databind.**
#-keepnames class com.fasterxml.jackson.** { *; }

# for gson see their documentation at
# https://code.google.com/p/google-gson/source/browse/trunk/examples/android-proguard-example/proguard.cfg

Configuration

Gradle and Android Studio

Configure your build.gradle

buildscript {
    repositories {
      mavenCentral()
    }
    dependencies {
        // Android plugin
        classpath 'com.android.tools.build:gradle:2.2.3'
        // the latest version of the android-apt plugin from https://bitbucket.org/hvisser/android-apt
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
    }
}

apply plugin: 'com.android.application'
apply plugin: 'com.neenbedankt.android-apt'
		 
		 
repositories {
     mavenCentral();
}

dependencies {
    compile 'de.devland.esperandro:esperandro-api:2.4.1'
    apt 'de.devland.esperandro:esperandro:2.4.1'
	
    // optional, if we want to use object serialization but don't provide our own Serializer
    // compile 'de.devland.esperandro:esperandro-gson-addon:2.4.1'
	// or
	// compile 'de.devland.esperandro:esperandro-jackson-addon:2.4.1'
}

IntelliJ

Additionally to the gradle configuration enable annotation processing in the settings.
Build, Execution, Deployment -> Compiler -> Annotation Processors
Check "Enable annotation processing"

Binaries Overview

License

Copyright 2017 David Kunzler

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.