/PreferenceRoom

:truck: Android processing library for managing SharedPreferences persistence efficiently and structurally.

Primary LanguageJavaApache License 2.0Apache-2.0

PreferenceRoom

License API Android CI Android Weekly

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.

Who's using this library?

GithubFollows
Open Source
All-In-One
Open Source
Battle Comics
Product
Epoptia
Open Source
Sensemore
Octocat allinone battleent epoptia Sensemore

Download

Maven Central

Gradle

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

Table of Contents

PreferenceEntity

preferenceentity
@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

keyname
@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;

TypeConverter

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

BaseTypeConverter

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

preferencefunction
@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 + "!!!";
}

EncryptEntity

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

preferencecomponent
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();

Dependency Injection

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

Usage in Kotlin

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.

Incremetal annotation processing

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)

Non Existent Type Correction

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
}

Proguard Rules

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

Debugging with Stetho

We can debug SharedPreferences values which managed by PreferenceRoom using Stetho.
screenshot635705571

References

Find this library useful? ❤️

Support it by joining stargazers for this repository. ⭐

Sponsor ☕

If you feel like to sponsor me a coffee for my efforts, I would greatly appreciate it.

Buy Me A Coffee

License

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.