PreferenceRoom is an android annotation processor library to manage SharedPreferences
more efficiently and structurally.
PreferenceRoom was inspired by Architecture Components Room Persistence
and dagger.
PreferenceRoom integrates scattered SharedPreferences
as a single entity and it supports custom setter/getter functions with security algorithm.
Also this library provides simple dependency injection system, which is free from reflection, and fully-supported in kotlin project.
GithubFollows Open Source |
All-In-One Open Source |
Battle Comics Product |
Epoptia Open Source |
Sensemore |
---|---|---|---|---|
Add the codes below to your root build.gradle
file (not your module build.gradle file).
allprojects {
repositories {
mavenCentral()
}
}
And add the dependency below to your module's build.gradle
file.
dependencies {
implementation "com.github.skydoves:preferenceroom:1.2.2"
annotationProcessor "com.github.skydoves:preferenceroom-processor:1.2.2"
// in kotlin project use kapt instead of annotationProcessor
kapt "com.github.skydoves:preferenceroom-processor:1.2.2"
}
@PreferenceEntity
annotation makes SharedPreferences data as an entity.
Value in @PreferenceEntity
determines the entity name.
Entity's default name is determined by class name.
@PreferenceEntity("UserProfile")
public class Profile {
protected final boolean login = false;
@KeyName("nickname") protected final String userNickName = null;
@KeyName("visits") protected final int visitCount = 1;
@KeyName("userPet")
@TypeConverter(PetConverter.class)
protected Pet userPetInfo;
@PreferenceFunction("nickname")
public String putUserNickFunction(String nickname) {
return "Hello, " + nickname;
}
@PreferenceFunction("nickname")
public String getUserNickFunction(String nickname) {
return nickname + "!!!";
}
@PreferenceFunction("visits")
public int putVisitCountFunction(int count) {
return ++count;
}
}
After the build your project, Preference_(entity name)
class will be generated automatically.
Preference_UserProfile userProfile = Preference_UserProfile.getInstance(this); // gets instance of the UserProfile entity.
userProfile.putNickname("my nickname"); // puts a value in NickName.
userProfile.getNickname(); // gets a nickname value.
userProfile.containsNickname(); // checks nickname key value is exist or not in entity.
userProfile.removeNickname(); // removes nickname key value in entity.
userProfile.nicknameKeyName(); // returns nickname key name.
userProfile.getEntityName(); // returns UserProfile entity name;
userProfile.getkeyNameList(); // returns UserProfile entity's key name lists.
// or invoke static.
Preference_UserProfile.getInstance(this).putNickname("my nickname");
we can listen the changed value using OnChangedListener
.
onChanged
method will be invoked if we change the value using put
method.
userProfile.addNicknameOnChangedListener(new Preference_UserProfile.NicknameOnChangedListener() {
@Override
public void onChanged(String nickname) {
Toast.makeText(getBaseContext(), "onChanged :" + nickname, Toast.LENGTH_SHORT).show();
}
});
Auto-generated code is managed by singletons.
But we can manage more efficiently using PreferenceComponent and
Dependency Injection.
We can set SharedPreference to an entity as DefaultSharedPreferences using @DefaultPreference
annotation like below.
@DefaultPreference
@PreferenceEntity("ProfileWithDefault")
public class UserProfilewithDefaultPreference {
@KeyName("nickname")
protected final String userNickName = "skydoves";
// key name will be 'Login'. (login's camel uppercase)
protected final boolean login = false;
}
The ProfileWithDefault
entity from the example, will be initialized like below on PreferenceRoom
processor.
PreferenceManager.getDefaultSharedPreferences(context);
So we can connect with PreferenceFragmentCompat, PreferenceScreen or etc.
@KeyName
annotation can be used on an entity class's field.
The field's key name will be decided by field name, but we can customize the name as taste.
@KeyName("visits") // keyname will be Visits.
protected final int visitCount = 1;
SharedPreference persists only primitive type data.
But PreferenceRoom supports persistence obejct data using TypeConverter
annotation.
@TypeConverter
annotation should be annotated over an object field like below.
@TypeConverter(PetConverter.class)
protected Pet userPetInfo; // 'Pet' class field in an entity.
Below example is creating a converter class using Gson.
Converter class should extends the PreferenceTypeConverter<?>
class.
The basic principle is making object class to string data for persistence and getting the string data and recover.
convertObject
performs how to change class data to a string, convertType
performs how to recover class data from the string data.
public class PetConverter extends PreferenceTypeConverter<Pet> {
private final Gson gson;
// default constructor will be called by PreferenceRoom
public PetConverter() {
this.gson = new Gson();
}
@Override
public String convertObject(Pet pet) {
return gson.toJson(pet);
}
@Override
public Pet convertType(String string) {
return gson.fromJson(string, Pet.class);
}
}
We can generalize the converter using generic like below.
public class BaseGsonConverter<T> extends PreferenceTypeConverter<T> {
private final Gson gson;
public BaseGsonConverter(Class<T> clazz) {
super(clazz);
this.gson = new Gson();
}
@Override
public String convertObject(T object) {
return gson.toJson(object);
}
@Override
public T convertType(String string) {
return gson.fromJson(string, clazz);
}
}
So we can use the converter to any classes.
@KeyName("userinfo")
@TypeConverter(BaseGsonConverter.class)
protected PrivateInfo privateInfo;
@KeyName("userPet")
@TypeConverter(BaseGsonConverter.class)
protected Pet userPetInfo;
@PreferenceFunction
annotation processes pre/post-processing through getter and setter functions.
@PreferenceFunction
annotation's value decides a target. The target should be a keyName.
The function's naming convention is which should start with put
or get
prefix.
put_functionname_
is pre-processing getter function and get_functionname_
is postprocessing getter function.
@PreferenceFunction("nickname")
public String putUserNickFunction(String nickname) {
return "Hello, " + nickname;
}
@PreferenceFunction("nickname")
public String getUserNickFunction(String nickname) {
return nickname + "!!!";
}
SharedPreferences data are not safe from hacking even if private-mode.
There is a simple way to encrypt whole entity using @EncryptEntity
annotation.
It is based on AES128 encryption. So we should set the key value along 16 size length.
If put
method invoked, the value is encrypted automatically.
And if get
method invoked, it returns the decrypted value.
@EncryptEntity("1234567890ABCDFG")
@PreferenceEntity("UserProfile")
public class Profile {
}
Or we can customize encrypting and decrypting algorithm using PreferenceFunction
.
@PreferenceFunction("uuid")
public String putUuidFunction(String uuid) {
return SecurityUtils.encrypt(uuid);
}
@PreferenceFunction("uuid")
public String getUuidFunction(String uuid) {
return SecurityUtils.decrypt(uuid);
}
PreferenceComponent integrates entities. @PreferenceComponent
annotation should be annotated on an interface class.
We can integrate many entities as one component using entities value in @PreferenceComponent
annotation.
So we can initialize many entities at once through the component and get instances from the component.
And the instance of the PreferenceComponent
is managed by singleton by PreferenceRoom.
`PreferenceComponent's instance also singletons. And all entities instances are initialized when the component is initialized.
@PreferenceComponent(entities = {Profile.class, Device.class})
public interface UserProfileComponent {
}
After build your project, PreferenceComponent_(component's name)
class will be generated automatically.
The best-recommanded way to initialize component is initializing on Application class. So we can manage the component as a singleton instance.
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
PreferenceComponent_UserProfileComponent.init(this);
}
}
After initializing the component, we can access and use the instances of component and component's entities anywhere.
PreferenceComponent_UserProfileComponent component = PreferenceComponent_UserProfileComponent.getInstance();
Preference_UserProfile userProfile = component.UserProfile();
Preference_UserDevice userDevice = PreferenceComponent_UserProfileComponent.getInstance().UserDevice();
PreferenceRoom
supports simple dependency injection process with free from reflection using @InjectPreference
annotation. But If you want to use with dagger, check this reference.
Firstly we should declare some target classes which to be injected preference instances in PreferenceComponent
.
@PreferenceComponent(entities = {Profile.class, Device.class})
public interface UserProfileComponent {
// declares targets for the dependency injection.
void inject(MainActivity __);
void inject(LoginActivity __);
}
We should annotate InjectPreference annotation to preference entity or component class field which generated by PreferenceRoom.
The field's modifier should be public
.
@InjectPreference
public PreferenceComponent_UserProfileComponent component;
@InjectPreference
public Preference_UserProfile userProfile;
And the last, we should inject instances of entity and component to targets fields using inject
method from an instance of the component.
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
PreferenceComponent_UserProfileComponent.getInstance().inject(this);
}
It is similar to java project usage.
But the most important thing is which we should use an open
modifier on entity classes and PreferenceFunctions.
And the field's modifier should be @JvmField val
.
@PreferenceEntity("UserDevice")
open class Device {
@KeyName("version")
@JvmField val deviceVersion: String? = null
@KeyName("uuid")
@JvmField val userUUID: String? = null
@PreferenceFunction("uuid")
open fun putUuidFunction(uuid: String?): String? {
return SecurityUtils.encrypt(uuid)
}
@PreferenceFunction( "uuid")
open fun getUuidFunction(uuid: String?): String? {
return SecurityUtils.decrypt(uuid)
}
}
And the component usage is almost the same as Java examples.
@PreferenceComponent(entities = [Profile::class, Device::class])
interface UserProfileComponent {
// declare dependency injection targets.
fun inject(target: MainActivity)
fun inject(target: LoginActivity)
}
And the last, the usage of the dependency injection is the same as the java. but we should declare the component and entity field's modifier as lateinit var.
@InjectPreference
lateinit var component: PreferenceComponent_UserProfileComponent
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
PreferenceComponent_UserProfileComponent.getInstance().inject(this) // inject dependency injection to MainActivity.
Starting from version 1.3.30
, kapt supports incremental annotation processing as an experimental feature. Currently, annotation processing can be incremental only if all annotation processors being used are incremental.
Incremental annotation processing is enabled by default starting from version 1.3.50
.
PreferenceRoom
supports incremental annotation processing since version 1.1.9
and here is a Before/After example result of the enabled.
Before (23.758s) | After (18.779s) |
---|---|
If you encounter NonExistentClass
error at compile time, you should add below codes on your build.gradle.
Default, Kapt replaces every unknown type (including types for the generated classes) to NonExistentClass, but you can change this behavior. Add the additional flag to the build.gradle file to enable error type inferring in stubs:
kapt {
correctErrorTypes = true
}
# Retain generated class which implement PreferenceRoomImpl.
-keep public class ** implements com.skydoves.preferenceroom.PreferenceRoomImpl
# Prevent obfuscation of types which use PreferenceRoom annotations since the simple name
# is used to reflectively look up the generated Injector.
-keep class com.skydoves.preferenceroom.*
-keepclasseswithmembernames class * { @com.skydoves.preferenceroom.* <methods>; }
-keepclasseswithmembernames class * { @com.skydoves.preferenceroom.* <fields>; }
We can debug SharedPreferences values which managed by PreferenceRoom using Stetho.
Support it by joining stargazers for this repository. :star:
If you feel like to sponsor me a coffee for my efforts, I would greatly appreciate it.
Copyright 2017 skydoves
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.