A smarter SharedPreferences with Kotlin

Nick Rose
3 min readMar 21, 2020

--

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!

--

--