Instantly share code, notes, and snippets.
Created
May 13, 2025 06:47
-
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 kibotu/d4ca97924f18226b42cdb387492fca67 to your computer and use it in GitHub Desktop.
Advanced android kotlin shared preference delegate.
This file contains hidden or 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
import android.content.SharedPreferences | |
import androidx.core.content.edit | |
import de.check24.android.plugin.profis.events.shared.logger.EventsLogger | |
import kotlinx.coroutines.flow.Flow | |
import kotlinx.coroutines.flow.MutableStateFlow | |
import kotlin.properties.ReadWriteProperty | |
import kotlin.reflect.KProperty | |
/** | |
* A type-safe SharedPreferences delegate with enhanced features: | |
* - Thread-safe operations | |
* - Reactive updates via Flow | |
* - Memory caching (optional) | |
* - Custom serialization/deserialization support | |
* - Migration capabilities | |
* - Null value support | |
* - Automatic commit/apply options | |
* | |
* @param T The type of the property value | |
*/ | |
class SharedPreferenceDelegate<T> private constructor( | |
private val sharedPreferences: () -> SharedPreferences?, | |
private val key: String, | |
private val defaultValue: T, | |
private val isCachingEnabled: Boolean, | |
private val useCommit: Boolean, | |
private val serializer: ((T) -> String)?, | |
private val deserializer: ((String) -> T)?, | |
private val migrationStrategy: ((SharedPreferences, String) -> Unit)? | |
) : ReadWriteProperty<Any, T> { | |
private var cachedValue: T? = null | |
private val valueFlow = MutableStateFlow(defaultValue) | |
private val accessLock = Any() | |
/** | |
* Get the property value, either from cache or SharedPreferences. | |
*/ | |
@Suppress("UNCHECKED_CAST") | |
override fun getValue(thisRef: Any, property: KProperty<*>): T = synchronized(accessLock) { | |
if (isCachingEnabled && cachedValue != null) { | |
return cachedValue as T | |
} | |
val preferences = sharedPreferences() | |
if (preferences == null) { | |
EventsLogger.w("SharedPreferences is null - returning default value for key: $key") | |
return defaultValue | |
} | |
// Apply any migrations if needed | |
migrationStrategy?.invoke(preferences, key) | |
// Use custom deserializer for complex types if provided | |
val result = when { | |
deserializer != null -> { | |
if (preferences.contains(key)) { | |
val serializedValue = preferences.getString(key, null) | |
if (serializedValue != null) deserializer.invoke(serializedValue) else defaultValue | |
} else { | |
defaultValue | |
} | |
} | |
// Handle nullable types with special marker | |
defaultValue == null -> { | |
if (preferences.contains("${key}_is_null") && preferences.getBoolean("${key}_is_null", false)) { | |
null as T | |
} else { | |
readFromPreferences(preferences, key, null) ?: defaultValue | |
} | |
} | |
// Standard primitive types | |
else -> readFromPreferences(preferences, key, defaultValue) ?: defaultValue | |
} | |
if (isCachingEnabled) { | |
cachedValue = result | |
} | |
valueFlow.value = result | |
return result | |
} | |
/** | |
* Set the property value in SharedPreferences. | |
*/ | |
override fun setValue(thisRef: Any, property: KProperty<*>, value: T) = synchronized(accessLock) { | |
val preferences = sharedPreferences() | |
if (preferences == null) { | |
EventsLogger.w("SharedPreferences is null - can't save value for key: $key") | |
return | |
} | |
// If the value is the same as cached, avoid unnecessary write | |
if (isCachingEnabled && cachedValue == value) { | |
return | |
} | |
preferences.edit(commit = useCommit) { | |
when { | |
// Use custom serializer for complex types if provided | |
serializer != null -> { | |
val serialized = serializer.invoke(value) | |
putString(key, serialized) | |
} | |
// Handle null values with a special marker | |
value == null -> { | |
putBoolean("${key}_is_null", true) | |
remove(key) | |
} | |
// Standard primitive types | |
else -> writeToPreferences(this, key, value) | |
} | |
} | |
cachedValue = value | |
valueFlow.value = value | |
} | |
/** | |
* Observe changes to this preference value as a Flow. | |
* @return A Flow that emits the current value and subsequent updates | |
*/ | |
fun asFlow(): Flow<T> = valueFlow | |
/** | |
* Invalidate the current cached value, forcing the next read to fetch from SharedPreferences. | |
*/ | |
fun invalidateCache() = synchronized(accessLock) { | |
cachedValue = null | |
} | |
/** | |
* Delete this preference from SharedPreferences. | |
*/ | |
fun delete() = synchronized(accessLock) { | |
val preferences = sharedPreferences() ?: return | |
preferences.edit(commit = useCommit) { | |
remove(key) | |
remove("${key}_is_null") | |
} | |
cachedValue = null | |
valueFlow.value = defaultValue | |
} | |
@Suppress("UNCHECKED_CAST") | |
private fun readFromPreferences(preferences: SharedPreferences, key: String, defaultValue: T?): T? { | |
if (!preferences.contains(key)) return null | |
return when (defaultValue) { | |
is Boolean?, is Boolean -> preferences.getBoolean(key, (defaultValue as? Boolean) ?: false) as T? | |
is Int?, is Int -> preferences.getInt(key, (defaultValue as? Int) ?: 0) as T? | |
is Long?, is Long -> preferences.getLong(key, (defaultValue as? Long) ?: 0L) as T? | |
is Float?, is Float -> preferences.getFloat(key, (defaultValue as? Float) ?: 0f) as T? | |
is String?, is String -> preferences.getString(key, defaultValue as? String) as T? | |
is Set<*>?, is Set<*> -> preferences.getStringSet(key, defaultValue as? Set<String>) as T? | |
else -> { | |
if (defaultValue == null) null | |
else throw IllegalArgumentException("Type not supported: ${defaultValue::class.java.name}") | |
} | |
} | |
} | |
private fun writeToPreferences(editor: SharedPreferences.Editor, key: String, value: T) { | |
when (value) { | |
is Boolean -> editor.putBoolean(key, value) | |
is Int -> editor.putInt(key, value) | |
is Long -> editor.putLong(key, value) | |
is Float -> editor.putFloat(key, value) | |
is String -> editor.putString(key, value) | |
is Set<*> -> @Suppress("UNCHECKED_CAST") editor.putStringSet(key, value as Set<String>) | |
else -> throw IllegalArgumentException("Type not supported: ${value::class.java.name}") | |
} | |
} | |
/** | |
* Builder for creating SharedPreferenceDelegate instances. | |
*/ | |
class Builder<T>( | |
private val sharedPreferences: () -> SharedPreferences?, | |
private val defaultValue: T | |
) { | |
private var key: String? = null | |
private var isCachingEnabled: Boolean = true | |
private var useCommit: Boolean = false | |
private var serializer: ((T) -> String)? = null | |
private var deserializer: ((String) -> T)? = null | |
private var migrationStrategy: ((SharedPreferences, String) -> Unit)? = null | |
/** | |
* Set a custom key for the preference. | |
* @param key Custom key to use instead of property name | |
*/ | |
fun key(key: String) = apply { this.key = key } | |
/** | |
* Enable or disable in-memory caching. | |
* @param enabled Whether caching should be enabled | |
*/ | |
fun withCaching(enabled: Boolean) = apply { this.isCachingEnabled = enabled } | |
/** | |
* Use commit() instead of apply() for immediate writes. | |
* @param useCommit Whether to use commit instead of apply | |
*/ | |
fun useCommit(useCommit: Boolean) = apply { this.useCommit = useCommit } | |
/** | |
* Provide custom serialization for complex types. | |
* @param serializer Function to convert value to String | |
* @param deserializer Function to convert String back to value | |
*/ | |
fun withCustomSerialization( | |
serializer: (T) -> String, | |
deserializer: (String) -> T | |
) = apply { | |
this.serializer = serializer | |
this.deserializer = deserializer | |
} | |
/** | |
* Provide a migration strategy for this preference. | |
* @param strategy Function to migrate from old format to new | |
*/ | |
fun withMigration(strategy: (SharedPreferences, String) -> Unit) = apply { | |
this.migrationStrategy = strategy | |
} | |
/** | |
* Build the SharedPreferenceDelegate instance. | |
* @param propertyName Name of the property (used if key is not set) | |
*/ | |
fun build(propertyName: String): SharedPreferenceDelegate<T> { | |
return SharedPreferenceDelegate( | |
sharedPreferences = sharedPreferences, | |
key = key ?: propertyName, | |
defaultValue = defaultValue, | |
isCachingEnabled = isCachingEnabled, | |
useCommit = useCommit, | |
serializer = serializer, | |
deserializer = deserializer, | |
migrationStrategy = migrationStrategy | |
) | |
} | |
} | |
companion object { | |
/** | |
* Create a builder for SharedPreferenceDelegate. | |
* @param sharedPreferences Lambda providing SharedPreferences instance | |
* @param defaultValue Default value if preference is not found | |
*/ | |
fun <T> builder( | |
sharedPreferences: () -> SharedPreferences?, | |
defaultValue: T | |
): Builder<T> { | |
return Builder(sharedPreferences, defaultValue) | |
} | |
} | |
} | |
/** | |
* Create a type-safe [ReadWriteProperty] backed by SharedPreferences. | |
* @param defaultValue Default value if preference is not found | |
* @param key Optional custom key (defaults to property name) | |
* @param isCachingEnabled Whether in-memory caching should be enabled | |
* @param useCommit Whether to use commit() instead of apply() | |
* @param sharedPreferences Lambda providing the SharedPreferences instance | |
* @return A property delegate for the specified type | |
*/ | |
fun <T> sharedPreference( | |
defaultValue: T, | |
key: String? = null, | |
isCachingEnabled: Boolean = true, | |
useCommit: Boolean = false, | |
sharedPreferences: () -> SharedPreferences? | |
): ReadWriteProperty<Any, T> { | |
return object : ReadWriteProperty<Any, T> { | |
private var delegate: SharedPreferenceDelegate<T>? = null | |
override fun getValue(thisRef: Any, property: KProperty<*>): T { | |
if (delegate == null) { | |
delegate = SharedPreferenceDelegate.builder(sharedPreferences, defaultValue) | |
.key(key ?: property.name) | |
.withCaching(isCachingEnabled) | |
.useCommit(useCommit) | |
.build(property.name) | |
} | |
return delegate!!.getValue(thisRef, property) | |
} | |
override fun setValue(thisRef: Any, property: KProperty<*>, value: T) { | |
if (delegate == null) { | |
delegate = SharedPreferenceDelegate.builder(sharedPreferences, defaultValue) | |
.key(key ?: property.name) | |
.withCaching(isCachingEnabled) | |
.useCommit(useCommit) | |
.build(property.name) | |
} | |
delegate!!.setValue(thisRef, property, value) | |
} | |
} | |
} | |
/** | |
* Create a property delegate with custom serialization/deserialization. | |
* @param defaultValue Default value if preference is not found | |
* @param key Optional custom key (defaults to property name) | |
* @param serializer Function to convert value to String | |
* @param deserializer Function to convert String back to value | |
* @param isCachingEnabled Whether in-memory caching should be enabled | |
* @param sharedPreferences Lambda providing the SharedPreferences instance | |
* @return A property delegate for the specified complex type | |
*/ | |
fun <T> serializedSharedPreference( | |
defaultValue: T, | |
key: String? = null, | |
serializer: (T) -> String, | |
deserializer: (String) -> T, | |
isCachingEnabled: Boolean = true, | |
useCommit: Boolean = false, | |
sharedPreferences: () -> SharedPreferences? | |
): ReadWriteProperty<Any, T> { | |
return object : ReadWriteProperty<Any, T> { | |
private var delegate: SharedPreferenceDelegate<T>? = null | |
override fun getValue(thisRef: Any, property: KProperty<*>): T { | |
if (delegate == null) { | |
delegate = SharedPreferenceDelegate.builder(sharedPreferences, defaultValue) | |
.key(key ?: property.name) | |
.withCaching(isCachingEnabled) | |
.useCommit(useCommit) | |
.withCustomSerialization(serializer, deserializer) | |
.build(property.name) | |
} | |
return delegate!!.getValue(thisRef, property) | |
} | |
override fun setValue(thisRef: Any, property: KProperty<*>, value: T) { | |
if (delegate == null) { | |
delegate = SharedPreferenceDelegate.builder(sharedPreferences, defaultValue) | |
.key(key ?: property.name) | |
.withCaching(isCachingEnabled) | |
.useCommit(useCommit) | |
.withCustomSerialization(serializer, deserializer) | |
.build(property.name) | |
} | |
delegate!!.setValue(thisRef, property, value) | |
} | |
} | |
} | |
/** | |
* Usage example: | |
* | |
* class MyPreferences(private val context: Context) { | |
* private val prefs by lazy { context.getSharedPreferences("my_prefs", Context.MODE_PRIVATE) } | |
* | |
* // Basic usage | |
* var userId by sharedPreference( | |
* defaultValue = "", | |
* sharedPreferences = { prefs } | |
* ) | |
* | |
* // Complex object with custom serialization | |
* var userProfile by serializedSharedPreference( | |
* defaultValue = UserProfile(), | |
* serializer = { Gson().toJson(it) }, | |
* deserializer = { Gson().fromJson(it, UserProfile::class.java) }, | |
* sharedPreferences = { prefs } | |
* ) | |
* | |
* // Advanced usage with builder pattern | |
* private val _darkMode = SharedPreferenceDelegate.builder( | |
* sharedPreferences = { prefs }, | |
* defaultValue = false | |
* ) | |
* .key("theme_dark_mode") | |
* .withCaching(true) | |
* .useCommit(true) | |
* .withMigration { prefs, _ -> | |
* // Migrate from old key if needed | |
* if (prefs.contains("old_dark_mode_key") && !prefs.contains("theme_dark_mode")) { | |
* prefs.edit { | |
* putBoolean("theme_dark_mode", prefs.getBoolean("old_dark_mode_key", false)) | |
* remove("old_dark_mode_key") | |
* } | |
* } | |
* } | |
* .build("darkMode") | |
* | |
* var darkMode by _darkMode | |
* val darkModeFlow = _darkMode.asFlow() | |
* | |
* fun resetDarkMode() { | |
* _darkMode.delete() | |
* } | |
* } | |
*/ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment