PreferenceRoom

Additional

Language
Java
Version
N/A
Created
Nov 20, 2017
Updated
Jul 25, 2018
Owner
Jaewoong Eum (skydoves)
Contributor
Jaewoong Eum (skydoves)
1
Activity
Badge
Generate
Download
Source code
APK file

Advertising

PreferenceRoom

Manage your project's SharedPreferences more efficiently. PreferenceRoom is inspired by Architecture Components Room Persistence and dagger. Fully supported in kotlin project. PreferenceRoom integrates scattered SharedPreferences as an entity. It supports putter & getter custom functions with security algorithm and could put & get objects. Also supports simple preference dependency injection with free from reflection.





Download

Gradle

dependencies {
    implementation 'com.github.skydoves:preferenceroom:1.1.0'
    annotationProcessor 'com.github.skydoves:preferenceroom-processor:1.1.0' // if android java project
    kapt 'com.github.skydoves:preferenceroom-processor:1.1.0' // if android kotlin project
}

Index

1.PreferenceEntity

2.PreferenceComponent

3.Dependency Injection

4.Usage in Kotlin

5.Proguard-Rules

6.Debugging with Stetho

PreferenceEntity

@PreferenceEntity annotation makes SharedPreferences data as an entity. "name" value in @PreferenceEntity determines entity's name. Entity's default name is determined by class name.



@PreferenceEntity(name = "UserProfile")
public class Profile {
    protected final boolean login = false;
    @KeyName(name = "nickname") protected final String userNickName = null;
    @KeyName(name = "visits") protected final int visitCount = 1;

    @KeyName(name = "userPet")
    @TypeConverter(converter = PetConverter.class)
    protected Pet userPetInfo;

    @PreferenceFunction(keyname = "nickname")
    public String putUserNickFunction(String nickname) {
        return "Hello, " + nickname;
    }

    @PreferenceFunction(keyname = "nickname")
    public String getUserNickFunction(String nickname) {
        return nickname + "!!!";
    }

    @PreferenceFunction(keyname = "visits")
    public int putVisitCountFunction(int count) {
        return ++count;
    }
}

After the build process, we can use Preference_(entity's name) class like following.

Preference_UserProfile userProfile = Preference_UserProfile.getInstance(this);
userProfile.putNickname("my nickname"); // puts a SharedPreference in NickName key.
userProfile.getNickname(); // gets a SharedPreference value in NickName key.
userProfile.containsNickname(); // checks nickname key value is exist in SharedPreference.
userProfile.removeNickname(); // removes nickname key's value in SharedPreference.
userProfile.nicknameKeyName(); // returns nickname fields's key name.
userProfile.getEntityName(); // returns UserProfile entity's name;
userProfile.getkeyNameList(); // returns UserProfile entity's KeyName list of fields.

// or invoke static.
Preference_UserProfile.getInstance(this).putNickname("my nickname");

auto-generated code is managed by singletons. but manage more efficiently using PreferenceComponent and Dependency Injection.


We can set SharedPreference as DefaultSharedPreferences using @DefaultPreference annotation like below.

@DefaultPreference
@PreferenceEntity(name = "ProfileWithDefault")
public class UserProfilewithDefaultPreference {
    @KeyName(name = "nickname")
    protected final String userNickName = "skydoves";

    /**
     * key name will be 'Login'. (login's camel uppercase)
     */
    protected final boolean login = false;

    // - skipped - //
}

Then "ProfileWithDefault" entity's instance will be initialized like below.

PreferenceManager.getDefaultSharedPreferences(context);

so we can connect with PreferenceActivity, PreferenceFragment or etc.

keyName

@KeyName annotation is used in an entity. @keyName's name value determines Sharedpreference's key name.

@KeyName(name = "visits") // keyname will be Visits.
protected final int visitCount = 1;

TypeConverter

You can put and get Objects using TypeConverter. @TypeConverter's converter value determines Converter.


@KeyName(name = "userPet")
@TypeConverter(converter = PetConverter.class)
protected Pet userPetInfo;

below example is converter using Gson. Converter should extends "PreferenceTypeConverter<?>" class.

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

This is a Base-Gson-Converter example. You can apply to all objects using generics like below.

public class BaseGsonConverter<T> extends PreferenceTypeConverter<T> {

    private final Gson gson;

    /**
     * default constructor will be called by PreferenceRoom
     */
    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);
    }
}

and using like this.

@KeyName(name = "userinfo")
@TypeConverter(converter = BaseGsonConverter.class)
protected PrivateInfo privateInfo;

@KeyName(name = "userPet")
@TypeConverter(converter = BaseGsonConverter.class)
protected Pet userPetInfo;

PreferenceFunction

@PreferenceFunction annotation processes getter and setter functions. @PreferenceFunction's "keyname" value determines a target. The target should be a keyName. Function's name should start with "put" or "get" prefix. put_functionname_ will processes getter function and get_functionname_ will processes getter function.



@PreferenceFunction(keyname = "nickname")
public String putUserNickFunction(String nickname) {
    return "Hello, " + nickname;
}

@PreferenceFunction(keyname = "nickname")
public String getUserNickFunction(String nickname) {
    return nickname + "!!!";
}

security

SharedPreferences data are not safe from hacking even if private-mode. When saving private-user data on SharedPreference, we can save by encrypting and decrypt algorithm with PreferenceFunction.

@PreferenceFunction(keyname = "uuid")
public String putUuidFunction(String uuid) {
   return SecurityUtils.encrypt(uuid);
}

@PreferenceFunction(keyname = "uuid")
public String getUuidFunction(String uuid) {
    return SecurityUtils.decrypt(uuid);
}

PreferenceComponent

PreferenceComponent integrates entities. @PreferenceComponent annotation is used on an interface. @PreferenceComponent's 'entities' values are targets to integrated by component. 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 the build process, can using PreferenceComponent_(component's name) class.

The best way to initialize component is initializing on Application class. because PreferenceRoom's instances are singleton process.

public class MyApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        PreferenceComponent_UserProfileComponent.init(this);
    }
}

After initializing on Application Class, we can access anywhere to component and component's entities.

Preference_UserProfile userProfile = PreferenceComponent_UserProfileComponent.getInstance().UserProfile();
Preference_UserDevice userDevice = PreferenceComponent_UserProfileComponent.getInstance().UserDevice();

Dependency Injection

All we know already about dependency injection. PreferenceRoom supplies simple dependency injection process with free from reflection.


first, we need to declare targets who will be injected by PreferenceRoom at Component.

@PreferenceComponent(entities = {Profile.class, Device.class})
public interface UserProfileComponent {
    /**
     * declare dependency injection targets.
     */
    void inject(MainActivity __);
    void inject(LoginActivity __);
}

and next, we should inject instances of entity and component at targets.

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   setContentView(R.layout.activity_main);
   PreferenceComponent_UserProfileComponent.getInstance().inject(this);
}

the last, request dependency injection using @InjectPreference annotation. As we know, field's modifier should be a public.

@InjectPreference
public PreferenceComponent_UserProfileComponent component;

@InjectPreference
public Preference_UserProfile userProfile;

Usage in Kotlin

First, create an entity. The most important thing is we shuold use open modifier at entity classes and PreferenceFunctions. And field's modifier should be @JvmField val.

@PreferenceEntity(name = "UserDevice")
open class Device {
    @KeyName(name = "version")
    @JvmField val deviceVersion: String? = null

    @KeyName(name = "uuid")
    @JvmField val userUUID: String? = null

    @PreferenceFunction(keyname = "uuid")
    open fun putUuidFunction(uuid: String?): String? {
        return SecurityUtils.encrypt(uuid)
    }

    @PreferenceFunction(keyname = "uuid")
    open fun getUuidFunction(uuid: String?): String? {
        return SecurityUtils.decrypt(uuid)
    }
}

Second, create a Component like below.

@PreferenceComponent(entities = arrayOf(Profile::class, Device::class))
interface UserProfileComponent {
    /**
     * declare dependency injection targets.
     */
    fun inject(target: MainActivity)
    fun inject(target: LoginActivity)
}

And the last, injecting is the same with the java. but we should declare component's modifier as lateinit var. That's it.

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

Non Existent Type Correction

But 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

You can debugging SharedPreferences values with Stetho. To view your app’s SharedPreferences, open the Resources tab of the Developer Tools window and select LocalStorage. You will see stored preferences by PreferenceRoom. Clicking a file displays the key-value pairs stored in that file.




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.