Skip to content

Instantly share code, notes, and snippets.

@marcardar
Last active July 12, 2020 05:36
Show Gist options
  • Save marcardar/2ea4b83b261abf58cdfc8e79b030c5fe to your computer and use it in GitHub Desktop.
Save marcardar/2ea4b83b261abf58cdfc8e79b030c5fe to your computer and use it in GitHub Desktop.
Access SharedPreferences using Kotlin property delegation. Also works on extension properties.
/**
* 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