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.
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
- esperandro - The annotation processor itself. Do not include in final packaged application, just use at compile time.
- esperandro-api - The annotations and interfaces needed at runtime. Include in the final package.
- esperandro-gson-addon - A gson-Serializer for object preferences. Include in the final package if necessary.
- esperandro-jackson-addon - A jackson-Serializer for object preferences. Include in the final package if necessary.
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.