Ditch the Boilerplate: The Kotlin Delegation Trick to Tame Android's SharedPreferences

 Tame Android’s SharedPreferences with Elegant, Type-Safe Properties

The Kotlin Delegation Trick to Tame Android's SharedPreferences

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 isLoaded calls a special getValue function on someDelegate.
  • Writing to isLoaded calls a special setValue function on someDelegate.

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 using apply().

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

Popular posts from this blog

Stop Writing Massive when Statements: Master the State Pattern in Kotlin

Coroutines & Flows: 5 Critical Anti-Patterns That Are Secretly Slowing Down Your Android App

Code Generation vs. Reflection: A Build-Time Reliability Analysis