Ditch the Boilerplate: The Kotlin Delegation Trick to Tame Android's SharedPreferences
Tame Android’s SharedPreferences with Elegant, Type-Safe Properties
If you’re an Android developer working in Kotlin, you’re likely familiar with the dreaded boilerplate that comes with managing simple settings using SharedPreferences.
Every time you need to persist a boolean or retrieve a string, you write variations of:
// The old, verbose way
val editor = prefs.edit()
editor.putBoolean("IS_ENABLED", true)
editor.apply()
// ... Later ...
val isEnabled = prefs.getBoolean("IS_ENABLED", false)It’s functional, but it’s repetitive, error-prone (typos in keys!), and certainly not elegant.
What if you could treat your persisted settings just like regular Kotlin properties, with full type safety and zero extra functions?
This is where Kotlin Property Delegation steps in, transforming a noisy settings repository into a clean, concise, and highly readable class.
The Power of by: Understanding Delegation
In Kotlin, the by keyword allows a class property to hand off its getter and setter logic to a separate object, the delegate.
When you declare:
var isLoaded by someDelegate- Reading
isLoadedcalls a specialgetValuefunction onsomeDelegate. - Writing to
isLoadedcalls a specialsetValuefunction onsomeDelegate.
To make SharedPreferences our delegate, we just need to implement these two operator functions and have them handle the actual disk I/O.
The Elegant and Enhanced Solution (with a Factory)
To ensure we maintain type safety and configurability (allowing custom keys and defaults), we’ll implement the delegate using a private factory method.
This approach is the cleanest, most scalable way to implement the feature.
Step 1: The Delegate Factory and Builders
We need two essential imports: kotlin.reflect.KProperty to access the property name, and the crucial ReadWriteProperty interface.
// SettingsDelegates.kt
import android.content.SharedPreferences
import android.content.SharedPreferences.Editor
import kotlin.reflect.KProperty
import kotlin.properties.ReadWriteProperty // ⬅️ FIX: Missing Import Added
// 1. Extension property for better discoverability
val SharedPreferences.delegates: PreferenceDelegates
get() = PreferenceDelegates(this) // NOTE: Creates a new object on each access, which is fine for simple usage.
class PreferenceDelegates(private val prefs: SharedPreferences) {
// --- Delegate Functions (exposed to the user) ---
fun boolean(default: Boolean = false, key: String? = null) = create(
default = default,
key = key, // ⬅️ FIX: key parameter passed
getter = { k, d -> prefs.getBoolean(k, d) },
setter = { k, v -> prefs.edit().putBoolean(k, v) }
)
fun string(default: String, key: String? = null) = create(
default = default,
key = key, // ⬅️ FIX: key parameter passed
getter = { k, d -> prefs.getString(k, d) ?: d },
setter = { k, v -> prefs.edit().putString(k, v) }
)
// ⬅️ FIX: long() delegate implemented
fun long(default: Long = 0L, key: String? = null) = create(
default = default,
key = key, // ⬅️ FIX: key parameter passed
getter = { k, d -> prefs.getLong(k, d) },
setter = { k, v -> prefs.edit().putLong(k, v) }
)
// --- Private Factory Function (The engine) ---
// ⬅️ FIX: key parameter added to factory function signature
private fun <T> create(
default: T,
key: String?,
getter: (key: String, default: T) -> T,
setter: (key: String, value: T) -> Editor,
) = object : ReadWriteProperty<Any?, T> { // Implementation object
// Helper to determine the key: use provided key or the property name
private fun getKey(property: KProperty<*>) = key ?: property.name
override fun getValue(thisRef: Any?, property: KProperty<*>): T {
// Read: Delegate the call to the type-specific getter lambda
return getter(getKey(property), default)
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
// Write: Delegate the call to the type-specific setter lambda and apply change
setter(getKey(property), value).apply()
}
}
}Step 2: Usage is Beautifully Simple
Now, the settings class is clean, readable, and highly maintainable.
// ExampleUsage.kt
class AppSettings(context: Context) {
// Get the standard SharedPreferences instance
private val prefs = context.getSharedPreferences("app_settings", Context.MODE_PRIVATE)
/**
* Setting with an explicit key for safe refactoring.
*/
var isBetaTester: Boolean by prefs.delegates.boolean(
default = false,
key = "PREF_BETA_TESTER" // The key stored on disk
)
/**
* Setting using the property name ("currentThemeId") as the key.
*/
var currentThemeId: String by prefs.delegates.string(
default = "light_theme"
)
/**
* FIX: The implemented long() delegate is now correctly used.
*/
var sessionTimeoutMillis: Long by prefs.delegates.long(5000L)
// --- Accessing the settings ---
fun toggleBeta() {
isBetaTester = !isBetaTester // Clean read/write access
}
}Performance & Modern Nuance
While this pattern is elegant, it’s vital to use it judiciously.
The “Chatty” Interface (Refined Wording)
The key to SharedPreferences performance is understanding where the data lives:
- Reads (
getValue): When you read a delegated property, the value is served directly from an in-memory map maintained by the Android system. This is very fast. - Writes (
setValue): When you write, the changes are written to the in-memory map instantly and then written to disk asynchronously usingapply().
My Recommendation: Use this delegation for simple, global settings. For large, structured, or transaction-heavy data, the overhead of constant memory and asynchronous disk updates is too high. You should use dedicated databases like Room.
Modern Android Alternative
For a 2025 development context, it’s important to note:
Jetpack DataStore is now the recommended replacement for SharedPreferences in new applications.
DataStore offers better thread safety and works with Kotlin Flows for reactive data, making it a superior choice for many projects. However, the delegation pattern presented here remains:
- A perfectly valid and clean solution for existing projects using
SharedPreferences. - An excellent way to learn and practice powerful Kotlin property delegation concepts.
Frequently Asked Questions (FAQs)
Why explicitly provide a key when the property name works?
Using the property name (property.name) as the key works by default. However, explicitly providing a key (e.g., "PREF_BETA_TESTER") makes refactoring safer. If you rename the property from isBetaTester to isEarlyAccess, the key stored on disk remains the same, preventing users from losing their setting.
Can I use commit() instead of apply()?
Yes, you can, but the delegate uses apply() by default because it performs the disk write asynchronously, preventing UI thread blocking. Using commit() makes the write synchronous, which should only be used if you must guarantee the write completed before launching another component (e.g., a critical safety check before quitting the app).
Does this support custom objects (like a user profile)?
Not directly with the basic delegates. The standard SharedPreferences API only supports primitives and strings. To store custom objects or lists, you would need to use a single string() delegate and handle the object's serialization (converting it to a JSON string using Gson/Moshi) within the delegate's getValue and setValue logic.
Is the creation of the PreferenceDelegates object a concern?
The extension property:
val SharedPreferences.delegates: PreferenceDelegates get() = PreferenceDelegates(this)creates a new PreferenceDelegates instance every time you access it. This is usually not a problem because you only access it once during the initialization of your AppSettings class. It's safe and idiomatic.
Conclusion
By applying Kotlin’s property delegation, we’ve transformed the tedious I/O of SharedPreferences into a beautiful, type-safe, and highly readable API. You gain maintainability and compile-time safety—all using features baked right into the Kotlin language.
To the reader: What is the most verbose piece of Android boilerplate you’ve successfully eliminated using a Kotlin feature like delegation or extension functions? Share your favorite trick in the comments!
If you find my content useful, feel free to support my work here. Every contribution is greatly appreciated! 🙏

Comments
Post a Comment