Instantly share code, notes, and snippets.
Last active
July 12, 2020 05:36
-
Star
(0)
0
You must be signed in to star a gist -
Fork
(0)
0
You must be signed in to fork a gist
-
Save marcardar/2ea4b83b261abf58cdfc8e79b030c5fe to your computer and use it in GitHub Desktop.
Access SharedPreferences using Kotlin property delegation. Also works on extension properties.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 5 scenarios for any settings-type class using SharedPreferences: | |
* | |
* 1. implements SharedPreferences | |
* 2. contains private SharedPreferences val (and does not expose it in any way) | |
* 3. contains public SharedPreferences val | |
* 4. companion object extends our SharedPreferencesProperties class | |
* 5. extends our SimpleSharedPreferencesProperties class | |
* | |
* Example usage: | |
class Settings1 : SharedPreferences { | |
var myInt by int("myPrefKey1") | |
... | |
} | |
class MyClassUsingSettings1 { | |
val settings1 = Settings1(...) | |
var myIntOther by settings1.int("myPrefKeyOther") | |
} | |
// two ways | |
var Settings1.myIntExt1 by SharedPreferencesProperties.int("myPrefKeyExt1") | |
var Settings1.myIntExt2 by PreferencesGetterSetter.int("myPrefKeyExt2").asProperty() | |
class Settings2(private val sharedPreferences: SharedPreferences) { | |
var myInt by sharedPreferences.int("myPrefKey") | |
} | |
class Settings3(val sharedPreferences: SharedPreferences) { | |
var myInt by sharedPreferences.int("myPrefKey") | |
} | |
class MyClassUsingSettings3 { | |
val settings3 = Settings3(...) | |
var myIntOther by settings3.sharedPreferences.int("myPrefKeyOther") | |
} | |
// two ways | |
var Settings3.myIntExt1 by SharedPreferencesProperties<Settings3> { sharedPreferences }.int("myPrefKeyExt1") | |
var Settings3.myIntExt2 by PreferencesGetterSetter.int("myPrefKey3Ext2") | |
.asProperty { sharedPreferences } | |
class Settings4(private val sharedPreferences: SharedPreferences) { | |
var myInt by int("myPrefKey") | |
// so that other classes can declare properties using the underlying sharedPreferences | |
// and extension functions can declare properties on this class using the underlying sharedPreferences | |
companion object : SharedPreferencesProperties<Settings4>({ sharedPreferences }) | |
} | |
class MyClassUsingSettings4 { | |
val settings4 = Settings4(...) | |
var myIntOther by Settings4.of(settings4).int("myPrefKeyOther") | |
} | |
var Settings4.myIntExt by Settings4.int("myPrefKeyExt") | |
class Settings5(sharedPreferences: SharedPreferences) : | |
SimpleSharedPreferencesProperties(sharedPreferences) { | |
var myInt by int("myPrefKey") | |
} | |
class MyClassUsingSettings5 { | |
val settings5 = Settings5(...) | |
var myIntOther by settings5.int("myPrefKeyOther") | |
} | |
// two ways | |
var Settings5.myIntExt1 by SimpleSharedPreferencesProperties.int("myPrefKeyExt1") | |
var Settings5.myIntExt2 by PreferencesGetterSetter.int("myPrefKeyExt2") | |
.asPropertyOfSimpleSharedPreferencesProperties() | |
*/ | |
import android.content.SharedPreferences | |
import androidx.core.content.edit | |
import kotlin.properties.ReadWriteProperty | |
import kotlin.reflect.KProperty | |
fun SharedPreferences.int(key: String, defaultValue: Int = 0) = | |
PreferencesGetterSetter.int(key, defaultValue).asProperty(this) | |
fun SharedPreferences.long(key: String, defaultValue: Long = 0L) = | |
PreferencesGetterSetter.long(key, defaultValue).asProperty(this) | |
fun SharedPreferences.boolean(key: String, defaultValue: Boolean = false) = | |
PreferencesGetterSetter.boolean(key, defaultValue).asProperty(this) | |
fun SharedPreferences.stringOrNull(key: String) = | |
PreferencesGetterSetter.stringOrNull(key).asProperty(this) | |
fun SharedPreferences.string(key: String, defaultValue: String) = | |
PreferencesGetterSetter.string(key, defaultValue).asProperty(this) | |
fun SharedPreferences.string(key: String, defaultValueLambda: SharedPreferences.() -> String) = | |
PreferencesGetterSetter.string(key, defaultValueLambda).asProperty(this) | |
fun SharedPreferences.nonBlankStringOrNull(key: String) = | |
PreferencesGetterSetter.nonBlankStringOrNull(key).asProperty(this) | |
fun SharedPreferences.stringSet(key: String, defaultValue: Set<String>? = null) = | |
PreferencesGetterSetter.stringSet(key, defaultValue).asProperty(this) | |
fun SharedPreferences.idNamePair(idKey: String, nameKey: String) = | |
PreferencesGetterSetter.idNamePair(idKey, nameKey).asProperty(this) | |
inline fun <reified T : Enum<T>> SharedPreferences.enum(key: String) = | |
PreferencesGetterSetter.enum<T>(key).asProperty(this) | |
inline fun <reified T : Enum<T>> SharedPreferences.enum(key: String, defaultValue: T) = | |
PreferencesGetterSetter.enum(key, defaultValue).asProperty(this) | |
inline fun <reified T : Enum<T>> SharedPreferences.enum( | |
key: String, | |
noinline defaultValueLambda: SharedPreferences.() -> T | |
) = PreferencesGetterSetter.enum(key, defaultValueLambda).asProperty(this) | |
fun <T> SharedPreferences.customUsingString( | |
key: String, | |
defaultValue: T, | |
fromRawValue: (String) -> T?, | |
toRawValue: (T) -> String | |
) = PreferencesGetterSetter.customUsingString(key, defaultValue, fromRawValue, toRawValue) | |
.asProperty(this) | |
fun <T> SharedPreferences.customUsingNullableString( | |
key: String, | |
defaultValue: T, | |
fromRawValue: (String) -> T?, | |
toRawValue: (T) -> String? | |
) = PreferencesGetterSetter.customUsingNullableString(key, defaultValue, fromRawValue, toRawValue) | |
.asProperty(this) | |
interface PreferencesGetterSetter<T> { | |
fun SharedPreferences.getValue(): T | |
/** | |
* @return true iff the value was accepted | |
*/ | |
fun SharedPreferences.Editor.setValue(value: T): Boolean | |
companion object { | |
fun int(key: String, defaultValue: Int = 0): PreferencesGetterSetter<Int> = | |
SimplePreferencesGetterSetter( | |
key = key, | |
defaultValue = defaultValue, | |
prefsGetter = SharedPreferences::getInt, | |
prefsSetter = SharedPreferences.Editor::putInt | |
) | |
fun long(key: String, defaultValue: Long): PreferencesGetterSetter<Long> = | |
SimplePreferencesGetterSetter( | |
key = key, | |
defaultValue = defaultValue, | |
prefsGetter = SharedPreferences::getLong, | |
prefsSetter = SharedPreferences.Editor::putLong | |
) | |
fun nonNegativeLong(key: String, defaultValue: Long = 0L) = | |
ConditionalGetterSetter(long(key, defaultValue)) { | |
it >= 0L | |
} | |
fun idNamePair( | |
idKey: String, | |
nameKey: String | |
): PreferencesGetterSetter<Pair<Long, String>?> = | |
GetterSetterPair(nonNegativeLong(idKey), nonBlankStringOrNull(nameKey)) | |
fun nonBlankStringOrNull(key: String) = ConditionalGetterSetter(stringOrNull(key)) { | |
it?.isBlank() != true | |
} | |
fun boolean(key: String, defaultValue: Boolean = false): PreferencesGetterSetter<Boolean> = | |
SimplePreferencesGetterSetter( | |
key = key, | |
defaultValue = defaultValue, | |
prefsGetter = SharedPreferences::getBoolean, | |
prefsSetter = SharedPreferences.Editor::putBoolean | |
) | |
fun stringOrNull(key: String): PreferencesGetterSetter<String?> = | |
SimplePreferencesGetterSetter( | |
key = key, | |
defaultValue = null, | |
prefsGetter = SharedPreferences::getString, | |
prefsSetter = SharedPreferences.Editor::putString | |
) | |
fun string(key: String, defaultValue: String) = | |
NonNullGetterSetter(stringOrNull(key), defaultValue) | |
fun string(key: String, defaultValueLambda: SharedPreferences.() -> String) = | |
NonNullGetterSetterDynamicDefault(stringOrNull(key), defaultValueLambda) | |
fun stringSet( | |
key: String, | |
defaultValue: Set<String>? = null | |
): PreferencesGetterSetter<Set<String>?> = | |
SimplePreferencesGetterSetter( | |
key = key, | |
defaultValue = defaultValue, | |
prefsGetter = SharedPreferences::getStringSet, | |
prefsSetter = SharedPreferences.Editor::putStringSet | |
) | |
fun <T> customUsingString( | |
key: String, | |
defaultValue: T, | |
fromRawValue: (String) -> T?, | |
toRawValue: (T) -> String | |
): PreferencesGetterSetter<T> = NonNullGetterSetter( | |
CustomNullableGetterSetter(stringOrNull(key), fromRawValue, toRawValue), | |
defaultValue | |
) | |
fun <T> customUsingNullableString( | |
key: String, | |
defaultValue: T, | |
fromRawValue: (String) -> T?, | |
toRawValue: (T) -> String? | |
) = NonNullGetterSetter( | |
CustomNullableGetterSetter( | |
stringOrNull(key), | |
{ it?.let { fromRawValue(it) } ?: defaultValue }, | |
toRawValue | |
), | |
defaultValue | |
) | |
inline fun <reified T : Enum<T>> enum(key: String) = | |
object : PreferencesGetterSetter<T?> { | |
override fun SharedPreferences.getValue(): T? { | |
return getString(key, null)?.let { valueStr -> | |
try { | |
enumValueOf<T>(valueStr) | |
} catch (e: Exception) { | |
// maybe it's a legacy case issue? | |
enumValues<T>().firstOrNull { it.name.equals(valueStr, true) } | |
} | |
} | |
} | |
override fun SharedPreferences.Editor.setValue(value: T?): Boolean { | |
if (value == null) { | |
remove(key) | |
} else { | |
putString(key, value.name) | |
} | |
return true | |
} | |
} | |
inline fun <reified T : Enum<T>> enum(key: String, defaultValue: T) = | |
NonNullGetterSetter(enum(key), defaultValue) | |
inline fun <reified T : Enum<T>> enum( | |
key: String, | |
noinline defaultValueLambda: SharedPreferences.() -> T | |
) = NonNullGetterSetterDynamicDefault(enum(key), defaultValueLambda) | |
} | |
} | |
private class SimplePreferencesGetterSetter<T>( | |
private val key: String, | |
private val defaultValue: T, | |
private val prefsGetter: SharedPreferences.(key: String, defaultValue: T) -> T, | |
private val prefsSetter: SharedPreferences.Editor.(key: String, value: T) -> SharedPreferences.Editor | |
) : PreferencesGetterSetter<T> { | |
override fun SharedPreferences.getValue() = prefsGetter(key, defaultValue) | |
override fun SharedPreferences.Editor.setValue(value: T): Boolean { | |
prefsSetter(key, value) | |
return true | |
} | |
} | |
class ConditionalGetterSetter<T>( | |
private val unconditionalGetterSetter: PreferencesGetterSetter<T>, | |
private val predicate: (T) -> Boolean | |
) : PreferencesGetterSetter<T?> { | |
override fun SharedPreferences.getValue(): T? = with(unconditionalGetterSetter) { | |
getValue().takeIf { predicate(it) } | |
} | |
override fun SharedPreferences.Editor.setValue(value: T?) = with(unconditionalGetterSetter) { | |
if (value != null && predicate(value)) { | |
setValue(value) | |
} else { | |
value == null | |
} | |
} | |
} | |
class NonNullGetterSetter<T>( | |
nullableGetterSetter: PreferencesGetterSetter<T?>, | |
private val defaultValue: T | |
) : NonNullGetterSetterBase<T>(nullableGetterSetter) { | |
override val SharedPreferences.defaultValue | |
get() = this@NonNullGetterSetter.defaultValue | |
} | |
abstract class NonNullGetterSetterBase<T>( | |
private val nullableGetterSetter: PreferencesGetterSetter<T?> | |
) : PreferencesGetterSetter<T> { | |
abstract val SharedPreferences.defaultValue: T | |
override fun SharedPreferences.getValue(): T = with(nullableGetterSetter) { | |
getValue() ?: defaultValue | |
} | |
override fun SharedPreferences.Editor.setValue(value: T) = with(nullableGetterSetter) { | |
setValue(value) | |
} | |
} | |
class NonNullGetterSetterDynamicDefault<T>( | |
nullableGetterSetter: PreferencesGetterSetter<T?>, | |
private val defaultValueLambda: SharedPreferences.() -> T | |
) : NonNullGetterSetterBase<T>(nullableGetterSetter) { | |
override val SharedPreferences.defaultValue: T | |
get() = defaultValueLambda() | |
} | |
private class CustomNullableGetterSetter<T, RAW>( | |
private val nullableGetterSetter: PreferencesGetterSetter<RAW?>, | |
private val fromRawValue: (RAW) -> T?, | |
private val toRawValue: (T) -> RAW | |
) : PreferencesGetterSetter<T?> { | |
override fun SharedPreferences.getValue(): T? = with(nullableGetterSetter) { | |
getValue()?.let { | |
try { | |
fromRawValue(it) | |
} catch (e: Exception) { | |
loge("failed to convert rawValue: $it") | |
null | |
} | |
} | |
} | |
override fun SharedPreferences.Editor.setValue(value: T?) = with(nullableGetterSetter) { | |
setValue(value?.let { toRawValue(it) }) | |
} | |
} | |
/** | |
* A gettersetter based on a Pair where Pair.first and Pair.second are non-null, but Pair can be null. | |
* firstGetterSetter and secondGetterSetter are nullable because they need to be null when the Pair is null. | |
*/ | |
private class GetterSetterPair<A, B>( | |
private val firstGetterSetter: PreferencesGetterSetter<A?>, | |
private val secondGetterSetter: PreferencesGetterSetter<B?> | |
) : PreferencesGetterSetter<Pair<A, B>?> { | |
override fun SharedPreferences.getValue(): Pair<A, B>? { | |
val first = with(firstGetterSetter) { getValue() } | |
val second = with(secondGetterSetter) { getValue() } | |
return (first ?: return null) to (second ?: return null) | |
} | |
override fun SharedPreferences.Editor.setValue(value: Pair<A, B>?): Boolean { | |
val firstSuccess = with(firstGetterSetter) { setValue(value?.first) } | |
// if first failed, then we have an unknown state for first value | |
val secondSuccess = if (firstSuccess) { | |
with(firstGetterSetter) { setValue(value?.first) } | |
} else { | |
false | |
} | |
if (!firstSuccess || !secondSuccess) { | |
// one or both failed so let's set both values to null for consistency | |
with(firstGetterSetter) { setValue(null) } | |
with(secondGetterSetter) { setValue(null) } | |
return false | |
} | |
return true | |
} | |
} | |
/** | |
* Intended for subclassing by companion objects so that we can do: | |
* | |
* int("key") instead of intGetterSetter(key).asProperty() | |
* | |
* Note: open so that companion object can extend it | |
*/ | |
open class SharedPreferencesProperties<R>( | |
private val sharedPreferencesLambda: (R.() -> SharedPreferences) | |
) : SharedPreferencesPropertiesBase<R>() { | |
override fun <T> PreferencesGetterSetter<T>.asProperty(): ReadWriteProperty<R, T> = | |
asProperty(sharedPreferencesLambda) | |
/** | |
* For when we don't want to specify the property on the reference | |
*/ | |
fun of(reference: R) = object : SharedPreferencesPropertiesBase<Any>() { | |
override fun <T> PreferencesGetterSetter<T>.asProperty(): ReadWriteProperty<Any, T> = | |
asProperty(reference.sharedPreferencesLambda()) | |
} | |
companion object: SharedPreferencesPropertiesBase<SharedPreferences>() { | |
private val simpleInstance by lazy { | |
SharedPreferencesProperties<SharedPreferences> { this } | |
} | |
override fun <T> PreferencesGetterSetter<T>.asProperty(): ReadWriteProperty<SharedPreferences, T> { | |
return with (simpleInstance) { | |
asProperty() | |
} | |
} | |
} | |
} | |
/** | |
* For when we don't need to declare extension properties. Our Settings class can extend this | |
* class and then this class, as well as all other classes with a reference to this class, have | |
* access to the various properties. | |
*/ | |
open class SimpleSharedPreferencesProperties(private val sharedPreferences: SharedPreferences) : | |
SharedPreferencesPropertiesBase<Any>() { | |
override fun <T> PreferencesGetterSetter<T>.asProperty() = asProperty(sharedPreferences) | |
companion object: SharedPreferencesPropertiesBase<SimpleSharedPreferencesProperties>() { | |
private val simpleInstance by lazy { | |
SharedPreferencesProperties<SimpleSharedPreferencesProperties> { sharedPreferences } | |
} | |
override fun <T> PreferencesGetterSetter<T>.asProperty(): ReadWriteProperty<SimpleSharedPreferencesProperties, T> { | |
return with (simpleInstance) { | |
asProperty() | |
} | |
} | |
} | |
} | |
abstract class SharedPreferencesPropertiesBase<R> { | |
abstract fun <T> PreferencesGetterSetter<T>.asProperty(): ReadWriteProperty<R, T> | |
fun int(key: String, defaultValue: Int = 0) = | |
PreferencesGetterSetter.int(key, defaultValue).asProperty() | |
fun long(key: String, defaultValue: Long = 0L) = | |
PreferencesGetterSetter.long(key, defaultValue).asProperty() | |
fun boolean(key: String, defaultValue: Boolean = false) = | |
PreferencesGetterSetter.boolean(key, defaultValue).asProperty() | |
fun stringOrNull(key: String) = | |
PreferencesGetterSetter.stringOrNull(key).asProperty() | |
fun string(key: String, defaultValue: String) = | |
PreferencesGetterSetter.string(key, defaultValue).asProperty() | |
fun string(key: String, defaultValueLambda: SharedPreferences.() -> String) = | |
PreferencesGetterSetter.string(key, defaultValueLambda).asProperty() | |
fun nonBlankStringOrNull(key: String) = | |
PreferencesGetterSetter.nonBlankStringOrNull(key).asProperty() | |
fun stringSet(key: String, defaultValue: Set<String>? = null) = | |
PreferencesGetterSetter.stringSet(key, defaultValue).asProperty() | |
fun idNamePair(idKey: String, nameKey: String) = | |
PreferencesGetterSetter.idNamePair(idKey, nameKey).asProperty() | |
inline fun <reified T : Enum<T>> enum(key: String) = | |
PreferencesGetterSetter.enum<T>(key).asProperty() | |
inline fun <reified T : Enum<T>> enum(key: String, defaultValue: T) = | |
PreferencesGetterSetter.enum(key, defaultValue).asProperty() | |
inline fun <reified T : Enum<T>> enum( | |
key: String, | |
noinline defaultValueLambda: SharedPreferences.() -> T | |
) = PreferencesGetterSetter.enum(key, defaultValueLambda).asProperty() | |
fun <T> customUsingString( | |
key: String, | |
defaultValue: T, | |
fromRawValue: (String) -> T?, | |
toRawValue: (T) -> String | |
) = PreferencesGetterSetter.customUsingString(key, defaultValue, fromRawValue, toRawValue) | |
.asProperty() | |
fun <T> customUsingNullableString( | |
key: String, | |
defaultValue: T, | |
fromRawValue: (String) -> T?, | |
toRawValue: (T) -> String? | |
) = PreferencesGetterSetter.customUsingNullableString( | |
key, | |
defaultValue, | |
fromRawValue, | |
toRawValue | |
).asProperty() | |
} | |
fun <T> PreferencesGetterSetter<T>.asProperty() = | |
object : ReadWriteProperty<SharedPreferences, T> { | |
override fun getValue(thisRef: SharedPreferences, property: KProperty<*>): T = | |
thisRef.getValue() | |
override fun setValue(thisRef: SharedPreferences, property: KProperty<*>, value: T) = | |
thisRef.edit { setValue(value) } | |
} | |
fun <T> PreferencesGetterSetter<T>.asPropertyOfSimpleSharedPreferencesProperties() = | |
object : ReadWriteProperty<SimpleSharedPreferencesProperties, T> { | |
override fun getValue( | |
thisRef: SimpleSharedPreferencesProperties, | |
property: KProperty<*> | |
): T = | |
with(thisRef) { | |
asProperty().getValue(thisRef, property) | |
} | |
override fun setValue( | |
thisRef: SimpleSharedPreferencesProperties, | |
property: KProperty<*>, | |
value: T | |
) = | |
with(thisRef) { | |
asProperty().setValue(thisRef, property, value) | |
} | |
} | |
fun <T> PreferencesGetterSetter<T>.asProperty(sharedPreferences: SharedPreferences) = | |
object : ReadWriteProperty<Any, T> { | |
override fun getValue(thisRef: Any, property: KProperty<*>): T = | |
sharedPreferences.getValue() | |
override fun setValue(thisRef: Any, property: KProperty<*>, value: T) = | |
sharedPreferences.edit { setValue(value) } | |
} | |
fun <R, T> PreferencesGetterSetter<T>.asProperty(sharedPreferencesLambda: R.() -> SharedPreferences) = | |
object : ReadWriteProperty<R, T> { | |
override fun getValue(thisRef: R, property: KProperty<*>): T = | |
thisRef.sharedPreferencesLambda().getValue() | |
override fun setValue(thisRef: R, property: KProperty<*>, value: T) = | |
thisRef.sharedPreferencesLambda().edit { setValue(value) } | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment