Every meaningful Android app will use SharedPreferences
at some point because it’s the simplest way to persist and get a persisted value. As a project grows in size along with the number of developers working on it, however, there risks being inconsistency in how SharedPreferences
is used — which can turn that simplicity on its head.
The APIs of SharedPreferences
aren’t exactly a dream to work with, either. As you can see in the code snippet below, an app can get quite noisy if it uses them exclusively. It’s also error-prone (e.g. getStringSet()
returns a (Mutable)Set<String>
that, when modified, mutates the set internal to the SharedPreferences
instance) and leaks implementation details (e.g. you have to remember the point of the edit()
and apply()
methods instead of something more intuitive like SharedPreferences.putX
).
sharedPreferences.getString(MY_SHARED_PREFERENCES_KEY, "default")
sharedPreferences.edit()
.putString(MY_SHARED_PREFERENCES_KEY, myString)
.apply()
The ideal state for the majority of apps is to have a singular instance of the SharedPreferences
class, scoped to the application, that all clients interface with. This is ideal because multiple instances can lead to an extremely confusing state where you put a value into one instance, and elsewhere in your app mistakenly ask a different instance for that value and don’t get out of it what you were expecting.
Using a dependency injection framework like Dagger or Koin is a typical way to expose that single instance and scope it accordingly, but you’re still left with the fixed, low-level APIs of SharedPreferences
. Having classes that are unrelated to preference management littered with its implementation details and all-caps key constants drags productivity down.
One way to hide those details is by wrapping your SharedPreferences
instance in a class that forwards app-specific APIs to it. Let’s say, for example, you want to count and expose the number of sessions a user has had. With this approach, the class that manages session counting might look like the following:
class SessionManager(
private val sharedPreferences: SharedPreferences
) {
val sessionCount: Int
get() = sharedPreferences.getInt(KEY_SESSION_COUNT, 0)
fun incrementSessionCount() {
sharedPreferences.edit()
.putInt(KEY_SESSION_COUNT, sessionCount + 1)
.apply()
}
companion object {
private const val KEY_SESSION_COUNT = "session_count"
}
}
This is also an approach which enables the scoping of APIs in order to hide mutability. For example, SessionManager
could implement an interface like so…
interface SessionCounter {
val sessionCount: Int fun isFirstSession() = sessionCount == 1
}
…and be provided as that SessionCounter
type to classes that only need to care about the current session count.
One of the projects I currently work on is over five years old and many classes use the SharedPreferences
APIs explicitly. Because of that, adapting the approach I described above to them isn’t as easy as a flick of your wrist. One way to make the transition a bit easier (or if you just feel like isolating all SharedPreferences
-related behaviours to a single class — something I wouldn’t condone for large apps) is to write a class that wraps your SharedPreferences
instance, but also implements the SharedPreferences
interface. This wrapper pattern typically explodes a class with loads of new methods to override, with each having to delegate to the wrapped instance (e.g. take a peek at the ContextWrapper
source code). Conveniently, however, the Kotlin language can hide all that boilerplate from us. Rather than explicitly overriding and forwarding methods, we can just use delegation to achieve the same thing with only a few keystrokes.
class AppPreferences(
sharedPreferences: SharedPreferences
) : SharedPreferences by sharedPreferences {
// App-specific code goes here
}
This might look a little funny, but it’s quite powerful. Our AppPreferences
class now has all the APIs of SharedPreferences
added to it without the boilerplate of delegating to the real implementation. AppPreferences
also no longer needs to have the SharedPreferences
instance as a field anymore (rather, just a parameter) because it itself is now a SharedPreferences
instance! Under the hood it’s all funnelling down to that parameter, however, via the delegation.
That’s it!