{"id":15626975,"url":"https://github.com/skydoves/preferenceroom","last_synced_at":"2025-04-06T10:12:03.016Z","repository":{"id":42461781,"uuid":"111425878","full_name":"skydoves/PreferenceRoom","owner":"skydoves","description":":truck: Android processing library for managing  SharedPreferences persistence efficiently and structurally.","archived":false,"fork":false,"pushed_at":"2022-08-07T07:37:27.000Z","size":564,"stargazers_count":377,"open_issues_count":2,"forks_count":26,"subscribers_count":9,"default_branch":"main","last_synced_at":"2025-03-30T08:12:30.259Z","etag":null,"topics":["android","android-library","annotation-processing","annotation-processor","annotations","dependency-injection","kotlin","kotlin-android","persistence","preferenceroom","preferences","shared-preferences","sharedpreferences","sharedpreferences-manager","skydoves","storage"],"latest_commit_sha":null,"homepage":"","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/skydoves.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":"CODEOWNERS","security":null,"support":null},"funding":{"github":"skydoves","custom":["https://www.paypal.me/skydoves","https://www.buymeacoffee.com/skydoves"]}},"created_at":"2017-11-20T15:10:52.000Z","updated_at":"2025-02-23T10:56:28.000Z","dependencies_parsed_at":"2022-08-12T10:00:48.266Z","dependency_job_id":null,"html_url":"https://github.com/skydoves/PreferenceRoom","commit_stats":null,"previous_names":[],"tags_count":13,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skydoves%2FPreferenceRoom","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skydoves%2FPreferenceRoom/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skydoves%2FPreferenceRoom/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skydoves%2FPreferenceRoom/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/skydoves","download_url":"https://codeload.github.com/skydoves/PreferenceRoom/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247464223,"owners_count":20942970,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["android","android-library","annotation-processing","annotation-processor","annotations","dependency-injection","kotlin","kotlin-android","persistence","preferenceroom","preferences","shared-preferences","sharedpreferences","sharedpreferences-manager","skydoves","storage"],"created_at":"2024-10-03T10:14:51.730Z","updated_at":"2025-04-06T10:12:02.977Z","avatar_url":"https://github.com/skydoves.png","language":"Java","funding_links":["https://github.com/sponsors/skydoves","https://www.paypal.me/skydoves","https://www.buymeacoffee.com/skydoves"],"categories":[],"sub_categories":[],"readme":"# PreferenceRoom \n\n[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)\n[![API](https://img.shields.io/badge/API-11%2B-brightgreen.svg?style=flat)](https://android-arsenal.com/api?level=11)\n[![Android CI](https://github.com/skydoves/PreferenceRoom/actions/workflows/android.yml/badge.svg)](https://github.com/skydoves/PreferenceRoom/actions/workflows/android.yml)\n[![Android Weekly](https://img.shields.io/badge/Android%20Weekly-%23335-orange.svg)](https://androidweekly.net/issues/issue-335)\n\u003cbr\u003e\n\nPreferenceRoom is an android annotation processor library to manage `SharedPreferences` more efficiently and structurally.\nPreferenceRoom was inspired by [Architecture Components Room Persistence](https://developer.android.com/topic/libraries/architecture/room.html)\nand [dagger](https://github.com/square/dagger).\nPreferenceRoom integrates scattered `SharedPreferences` as a single entity and it supports custom setter/getter functions with security algorithm.\nAlso this library provides simple dependency injection system, which is free from reflection, and fully-supported in kotlin project. \n\n\n## Who's using this library?\n\n| [GithubFollows](https://github.com/skydoves)\u003cbr\u003e[Open Source](https://github.com/skydoves/githubfollows) | [All-In-One](https://github.com/skydoves)\u003cbr\u003e[Open Source](https://github.com/skydoves/all-in-one) | [Battle Comics](http://www.battlecomics.co.kr/)\u003cbr\u003e[Product](https://play.google.com/store/apps/details?id=com.whalegames.app) | [Epoptia](http://epoptia.com/cloud-mes-manufacturing-execution-system/) \u003cbr\u003e [Open Source](https://github.com/tsironis13/EpoptiaKioskModeApp) | [Sensemore](https://sensemore.io/)\n| :---------------: | :---------------: | :---------------: | :---------------: | :---------------: |\n| ![Octocat](https://user-images.githubusercontent.com/24237865/61508303-38343180-aa24-11e9-8b26-43dd5332be98.png) | ![allinone](https://user-images.githubusercontent.com/24237865/61508304-38ccc800-aa24-11e9-8d43-c245b7278f5f.png) | ![battleent](https://user-images.githubusercontent.com/24237865/61508305-38ccc800-aa24-11e9-9d02-2b936e33f8cd.png) | ![epoptia](https://user-images.githubusercontent.com/24237865/61509215-7f242600-aa28-11e9-97fc-be53960d079b.png) | ![Sensemore](https://user-images.githubusercontent.com/24237865/71539660-ee605780-2982-11ea-964d-389604da8fac.png)\n\n## Download\n[![Maven Central](https://img.shields.io/maven-central/v/com.github.skydoves/preferenceroom.svg?label=Maven%20Central)](https://search.maven.org/search?q=g:%22com.github.skydoves%22%20AND%20a:%22preferenceroom%22)\n\n### Gradle\nAdd the codes below to your **root** `build.gradle` file (not your module build.gradle file).\n```gradle\nallprojects {\n    repositories {\n        mavenCentral()\n    }\n}\n```\nAnd add the dependency below to your **module**'s `build.gradle` file.\n\n```gradle\ndependencies {\n    implementation \"com.github.skydoves:preferenceroom:1.2.2\"\n    annotationProcessor \"com.github.skydoves:preferenceroom-processor:1.2.2\"\n    // in kotlin project use kapt instead of annotationProcessor\n    kapt \"com.github.skydoves:preferenceroom-processor:1.2.2\"\n}\n```\n\n## Table of Contents\n#### [1.PreferenceEntity](https://github.com/skydoves/PreferenceRoom#preferenceentity)\n- [KeyName](https://github.com/skydoves/PreferenceRoom#keyname)\n- [TypeConverter](https://github.com/skydoves/PreferenceRoom#typeconverter)\n- [PreferenceFunction](https://github.com/skydoves/PreferenceRoom#preferencefunction) \n- [EncryptEntity](https://github.com/skydoves/PreferenceRoom#encryptentity)\n#### [2.PreferenceComponent](https://github.com/skydoves/PreferenceRoom#preferencecomponent)\n#### [3.Dependency Injection](https://github.com/skydoves/PreferenceRoom#dependency-injection) (Use with [dagger](https://github.com/skydoves/PreferenceRoom/releases))\n#### [4.Usage in Kotlin](https://github.com/skydoves/PreferenceRoom#usage-in-kotlin) ([Incremental annotation processing](https://github.com/skydoves/PreferenceRoom#incremetal-annotation-processing))\n#### [5.Proguard-Rules](https://github.com/skydoves/PreferenceRoom#proguard-rules)\n#### [6.Debugging with Stetho](https://github.com/skydoves/PreferenceRoom#debugging-with-stetho)\n\n## PreferenceEntity\n![preferenceentity](https://user-images.githubusercontent.com/24237865/33240687-5fa9ccca-d2fd-11e7-8962-e39c8dad5f41.png)\u003cbr\u003e\n`@PreferenceEntity` annotation makes SharedPreferences data as an entity.\u003cbr\u003e\nValue in `@PreferenceEntity` determines the entity name.\u003cbr\u003e\nEntity's default name is determined by class name.\u003cbr\u003e\n\n```java\n@PreferenceEntity(\"UserProfile\")\npublic class Profile {\n    protected final boolean login = false;\n    @KeyName(\"nickname\") protected final String userNickName = null;\n    @KeyName(\"visits\") protected final int visitCount = 1;\n\n    @KeyName(\"userPet\")\n    @TypeConverter(PetConverter.class)\n    protected Pet userPetInfo;\n\n    @PreferenceFunction(\"nickname\")\n    public String putUserNickFunction(String nickname) {\n        return \"Hello, \" + nickname;\n    }\n\n    @PreferenceFunction(\"nickname\")\n    public String getUserNickFunction(String nickname) {\n        return nickname + \"!!!\";\n    }\n\n    @PreferenceFunction(\"visits\")\n    public int putVisitCountFunction(int count) {\n        return ++count;\n    }\n}\n```\n\nAfter the build your project, `Preference_(entity name)` class will be generated automatically. \u003cbr\u003e\n```java\nPreference_UserProfile userProfile = Preference_UserProfile.getInstance(this); // gets instance of the UserProfile entity.\nuserProfile.putNickname(\"my nickname\"); // puts a value in NickName.\nuserProfile.getNickname(); // gets a nickname value.\nuserProfile.containsNickname(); // checks nickname key value is exist or not in entity.\nuserProfile.removeNickname(); // removes nickname key value in entity.\nuserProfile.nicknameKeyName(); // returns nickname key name.\nuserProfile.getEntityName(); // returns UserProfile entity name;\nuserProfile.getkeyNameList(); // returns UserProfile entity's key name lists.\n\n// or invoke static.\nPreference_UserProfile.getInstance(this).putNickname(\"my nickname\");\n```\n\nwe can listen the changed value using `OnChangedListener`.\u003cbr\u003e\n`onChanged` method will be invoked if we change the value using `put` method.\n```java\nuserProfile.addNicknameOnChangedListener(new Preference_UserProfile.NicknameOnChangedListener() {\n   @Override\n   public void onChanged(String nickname) {\n     Toast.makeText(getBaseContext(), \"onChanged :\" + nickname, Toast.LENGTH_SHORT).show();\n   }\n});\n```\n\nAuto-generated code is managed by singletons. \u003c/br\u003e\nBut we can manage more efficiently using [PreferenceComponent](https://github.com/skydoves/PreferenceRoom#preferencecomponent) and\n[Dependency Injection](https://github.com/skydoves/PreferenceRoom#dependency-injection). \u003cbr\u003e\n\nWe can set SharedPreference to an entity as DefaultSharedPreferences using `@DefaultPreference` annotation like below.\n```java\n@DefaultPreference\n@PreferenceEntity(\"ProfileWithDefault\")\npublic class UserProfilewithDefaultPreference {\n    @KeyName(\"nickname\")\n    protected final String userNickName = \"skydoves\";\n    \n    // key name will be 'Login'. (login's camel uppercase)\n    protected final boolean login = false;\n}\n```\nThe `ProfileWithDefault` entity from the example, will be initialized like below on `PreferenceRoom` processor.\n```java\nPreferenceManager.getDefaultSharedPreferences(context);\n```\nSo we can connect with PreferenceFragmentCompat, PreferenceScreen or etc.\n\n### KeyName\n![keyname](https://user-images.githubusercontent.com/24237865/33240803-7c80bb7c-d2ff-11e7-98e4-cf43d6aebb1e.png)\u003cbr\u003e\n`@KeyName` annotation can be used on an entity class's field. \u003cbr\u003e\nThe field's key name will be decided by field name, but we can customize the name as taste. \u003cbr\u003e\n```java\n@KeyName(\"visits\") // keyname will be Visits.\nprotected final int visitCount = 1;\n```\n\n### TypeConverter\n![typeconverter](https://user-images.githubusercontent.com/24237865/33240860-c5b6495a-d300-11e7-8122-804993a07b4a.png) \u003cbr\u003e\nSharedPreference persists only primitive type data. \u003cbr\u003e\nBut PreferenceRoom supports persistence obejct data using `TypeConverter` annotation. \u003cbr\u003e\n`@TypeConverter` annotation should be annotated over an object field like below. \u003cbr\u003e\n```java\n@TypeConverter(PetConverter.class)\nprotected Pet userPetInfo; // 'Pet' class field in an entity.\n```\n\nBelow example is creating a converter class using Gson. \u003cbr\u003e\nConverter class should extends the `PreferenceTypeConverter\u003c?\u003e` class. \u003cbr\u003e\nThe basic principle is making object class to string data for persistence and getting the string data and recover.\u003cbr\u003e\n`convertObject` performs how to change class data to a string, `convertType` performs how to recover class data from the string data.\n```java\npublic class PetConverter extends PreferenceTypeConverter\u003cPet\u003e {\n\n    private final Gson gson;\n\n    // default constructor will be called by PreferenceRoom\n    public PetConverter() {\n        this.gson = new Gson();\n    }\n\n    @Override\n    public String convertObject(Pet pet) {\n        return gson.toJson(pet);\n    }\n\n    @Override\n    public Pet convertType(String string) {\n        return gson.fromJson(string, Pet.class);\n    }\n}\n```\n\n#### BaseTypeConverter\nWe can generalize the converter using generic like below. \u003cbr\u003e\n\n```java\npublic class BaseGsonConverter\u003cT\u003e extends PreferenceTypeConverter\u003cT\u003e {\n\n    private final Gson gson;\n\n    public BaseGsonConverter(Class\u003cT\u003e clazz) {\n        super(clazz);\n        this.gson = new Gson();\n    }\n\n    @Override\n    public String convertObject(T object) {\n        return gson.toJson(object);\n    }\n\n    @Override\n    public T convertType(String string) {\n        return gson.fromJson(string, clazz);\n    }\n}\n```\nSo we can use the converter to any classes.\n```java\n@KeyName(\"userinfo\")\n@TypeConverter(BaseGsonConverter.class)\nprotected PrivateInfo privateInfo;\n\n@KeyName(\"userPet\")\n@TypeConverter(BaseGsonConverter.class)\nprotected Pet userPetInfo;\n```\n\n### PreferenceFunction\n![preferencefunction](https://user-images.githubusercontent.com/24237865/33240543-c292ee82-d2fa-11e7-86c2-b013830965b2.png)\u003cbr\u003e\n`@PreferenceFunction` annotation processes pre/post-processing through getter and setter functions. \u003cbr\u003e\n`@PreferenceFunction` annotation's value decides a target. The target should be a keyName. \u003cbr\u003e\nThe function's naming convention is which should start with `put` or `get` prefix. \u003cbr\u003e\n`put_functionname_` is pre-processing getter function and `get_functionname_` is postprocessing getter function.\n\n```java\n@PreferenceFunction(\"nickname\")\npublic String putUserNickFunction(String nickname) {\n    return \"Hello, \" + nickname;\n}\n\n@PreferenceFunction(\"nickname\")\npublic String getUserNickFunction(String nickname) {\n    return nickname + \"!!!\";\n}\n```\n\n### EncryptEntity\nSharedPreferences data are not safe from hacking even if private-mode.\u003cbr\u003e\nThere is a simple way to encrypt whole entity using `@EncryptEntity` annotation.\u003cbr\u003e\nIt is based on AES128 encryption. So we should set the key value along __16 size__ length.\u003cbr\u003e\nIf `put` method invoked, the value is encrypted automatically. \u003cbr\u003e\nAnd if `get` method invoked, it returns the decrypted value.\n\n```java\n@EncryptEntity(\"1234567890ABCDFG\")\n@PreferenceEntity(\"UserProfile\")\npublic class Profile {\n}\n```\nOr we can customize encrypting and decrypting algorithm using `PreferenceFunction`.\n```java\n@PreferenceFunction(\"uuid\")\npublic String putUuidFunction(String uuid) {\n   return SecurityUtils.encrypt(uuid);\n}\n\n@PreferenceFunction(\"uuid\")\npublic String getUuidFunction(String uuid) {\n    return SecurityUtils.decrypt(uuid);\n}\n```\n\n## PreferenceComponent\n![preferencecomponent](https://user-images.githubusercontent.com/24237865/33240928-10a88e18-d302-11e7-8ff5-b5d4f33de692.png) \u003cbr\u003e\nPreferenceComponent integrates entities. `@PreferenceComponent` annotation should be annotated on an interface class.\u003cbr\u003e\nWe can integrate many entities as one component using entities value in `@PreferenceComponent` annotation. \u003cbr\u003e\nSo we can initialize many entities at once through the component and get instances from the component. \u003cbr\u003e\nAnd the instance of the `PreferenceComponent` is managed by singleton by PreferenceRoom.\n`PreferenceComponent's instance also singletons. And all entities instances are initialized when the component is initialized.\u003cbr\u003e\n```java\n@PreferenceComponent(entities = {Profile.class, Device.class})\npublic interface UserProfileComponent {\n}\n```\nAfter build your project, `PreferenceComponent_(component's name)` class will be generated automatically. \u003cbr\u003e\nThe best-recommanded way to initialize component is initializing on Application class. So we can manage the component as a singleton instance.\n\n```java\npublic class MyApplication extends Application {\n\n    @Override\n    public void onCreate() {\n        super.onCreate();\n        PreferenceComponent_UserProfileComponent.init(this);\n    }\n}\n\n```\nAfter initializing the component, we can access and use the instances of component and component's entities anywhere.\u003cbr\u003e\n```java\nPreferenceComponent_UserProfileComponent component = PreferenceComponent_UserProfileComponent.getInstance();\nPreference_UserProfile userProfile = component.UserProfile();\nPreference_UserDevice userDevice = PreferenceComponent_UserProfileComponent.getInstance().UserDevice();\n```\n\n## Dependency Injection\n![di](https://user-images.githubusercontent.com/24237865/33241294-bfaf9b5a-d306-11e7-816a-2be938fafdf8.png) \u003cbr\u003e\n`PreferenceRoom` supports simple dependency injection process with free from reflection using `@InjectPreference` annotation. But If you want to use with dagger, check [this](https://github.com/skydoves/PreferenceRoom/releases) reference.\n\nFirstly we should declare some target classes which to be injected preference instances in `PreferenceComponent`. \u003cbr\u003e\n```java\n@PreferenceComponent(entities = {Profile.class, Device.class})\npublic interface UserProfileComponent {\n    // declares targets for the dependency injection.\n    void inject(MainActivity __);\n    void inject(LoginActivity __);\n}\n```\n\nWe should annotate InjectPreference annotation to preference entity or component class field which generated by PreferenceRoom. \u003cbr\u003e\nThe field's modifier should be `public`.\n```java\n@InjectPreference\npublic PreferenceComponent_UserProfileComponent component;\n\n@InjectPreference\npublic Preference_UserProfile userProfile;\n```\n\nAnd the last, we should inject instances of entity and component to targets fields using `inject` method from an instance of the component.  \u003c/br\u003e\n```java\n@Override\nprotected void onCreate(@Nullable Bundle savedInstanceState) {\n   super.onCreate(savedInstanceState);\n   setContentView(R.layout.activity_main);\n   PreferenceComponent_UserProfileComponent.getInstance().inject(this);\n}\n```\n\n## Usage in Kotlin\nIt is similar to java project usage. \u003cbr\u003e\nBut the most important thing is which we should use an `open` modifier on entity classes and PreferenceFunctions. \u003cbr\u003e\nAnd the field's modifier should be `@JvmField val`.\n```java\n@PreferenceEntity(\"UserDevice\")\nopen class Device {\n    @KeyName(\"version\")\n    @JvmField val deviceVersion: String? = null\n\n    @KeyName(\"uuid\")\n    @JvmField val userUUID: String? = null\n\n    @PreferenceFunction(\"uuid\")\n    open fun putUuidFunction(uuid: String?): String? {\n        return SecurityUtils.encrypt(uuid)\n    }\n\n    @PreferenceFunction( \"uuid\")\n    open fun getUuidFunction(uuid: String?): String? {\n        return SecurityUtils.decrypt(uuid)\n    }\n}\n```\n\nAnd the component usage is almost the same as Java examples.\n```java\n@PreferenceComponent(entities = [Profile::class, Device::class])\ninterface UserProfileComponent {\n    // declare dependency injection targets.\n    fun inject(target: MainActivity)\n    fun inject(target: LoginActivity)\n}\n```\n\nAnd 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. \u003cbr\u003e\n```java\n@InjectPreference\nlateinit var component: PreferenceComponent_UserProfileComponent\n    \noverride fun onCreate(savedInstanceState: Bundle?) {\n   super.onCreate(savedInstanceState)\n   setContentView(R.layout.activity_main)\n    PreferenceComponent_UserProfileComponent.getInstance().inject(this) // inject dependency injection to MainActivity.\n```\n\n### Incremetal annotation processing\nStarting 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.\u003cbr\u003e\nIncremental annotation processing is enabled by default starting from version `1.3.50`.\u003cbr\u003e\n`PreferenceRoom` supports incremental annotation processing since version `1.1.9` and here is a Before/After example result of the enabled.\n\nBefore (23.758s) | After (18.779s) | \n| :---------------: | :---------------: | \n| \u003cimg src=\"https://user-images.githubusercontent.com/24237865/76435566-7b4f7480-63fa-11ea-999f-25133c577d07.png\" align=\"center\" width=\"100%\"/\u003e | \u003cimg src=\"https://user-images.githubusercontent.com/24237865/76435569-7d193800-63fa-11ea-9d75-dd0a3e9de68c.png\" align=\"center\" width=\"100%\"/\u003e |\n\n\n### Non Existent Type Correction\nIf you encounter `NonExistentClass` error at compile time, you should add below codes on your build.gradle. \u003cbr\u003e\nDefault, 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:\n```xml\nkapt {\n  correctErrorTypes = true\n}\n```\n\n## Proguard Rules\n```xml\n# Retain generated class which implement PreferenceRoomImpl.\n-keep public class ** implements com.skydoves.preferenceroom.PreferenceRoomImpl\n\n# Prevent obfuscation of types which use PreferenceRoom annotations since the simple name\n# is used to reflectively look up the generated Injector.\n-keep class com.skydoves.preferenceroom.*\n-keepclasseswithmembernames class * { @com.skydoves.preferenceroom.* \u003cmethods\u003e; }\n-keepclasseswithmembernames class * { @com.skydoves.preferenceroom.* \u003cfields\u003e; }\n```\n\n## Debugging with Stetho\nWe can debug SharedPreferences values which managed by PreferenceRoom using Stetho. \u003cbr\u003e \n![screenshot635705571](https://user-images.githubusercontent.com/24237865/43187949-e35f5812-902d-11e8-8aa9-c090b90e96c5.png)\n\n## References\n- [How to manage SharedPreferences on Android project more efficiently](https://medium.com/@skydoves/how-to-manage-sharedpreferences-on-android-project-5e6d5e28fee6)\n\n## Find this library useful? :heart:\nSupport it by joining __[stargazers](https://github.com/skydoves/PreferenceRoom/stargazers)__ for this repository. :star:\n\n## Sponsor :coffee:\nIf you feel like to sponsor me a coffee for my efforts, I would greatly appreciate it. \u003cbr\u003e\n\n\u003ca href=\"https://www.buymeacoffee.com/skydoves\" target=\"_blank\"\u003e\u003cimg src=\"https://skydoves.github.io/sponsor.png\" alt=\"Buy Me A Coffee\" style=\"height: 51px !important;width: 217px !important;\" \u003e\u003c/a\u003e\n\n# License\n```xml\nCopyright 2017 skydoves\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n   http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fskydoves%2Fpreferenceroom","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fskydoves%2Fpreferenceroom","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fskydoves%2Fpreferenceroom/lists"}