Property syntax for Android's SharedPreferences
or iOS/macOS NSUserDefaults
.
Supported platforms: macOS (x64), iOS (arm32, arm64 & x64), Android.
This library uses Kotlin's property delegation to make using
SharedPreferences as easy as accessing a property on an object, and provides an NSUserDefaults
backed implementation for macOS and iOS.
On Android, it relies on the appCtx
module of this library to allow usage in object
,
and can support storage on device encrypted storage for devices
supporting Direct Boot. See the source code for more information.
- Defining the preferences properties in an object
- Loading the preferences without blocking the main thread
- Download
Define your preferences in an object
that extends
splitties.preferences.Preferences
, like in the example below:
import splitties.preferences.Preferences
object GamePreferences : Preferences("gameState") {
var magicNumber by intPref(0) // The property name is used as the key.
var currentLevel by IntPref("currentLevel", 1)
var bossesFought by IntPref("bossBattleVictories", 0)
var lastTimePlayed by LongPref("lastSessionTime", 0L)
private val pseudoField = StringPref("playerPseudo", "Player 1")
var currentPseudo: String by pseudoField
val pseudoUpdates: Flow<String> = pseudoField.valueFlow()
var favoriteCharacter by stringOrNullPref()
}
Then just use the properties:
fun setResponseOfTheUltimateQuestionOfLifeTheUniverseAndEverything() {
GamePreferences.magicNumber = 42
}
fun doSomeMagic() {
toast("Magic: ${GamePreferences.magicNumber}!")
}
fun resetProgress() {
GamePreferences.edit { // Batch edit
currentLevel = 1
bossesFought = 0
}
}
The supported types are:
Boolean
Int
Float
Long
String
String?
Set<String>
Set<String>?
For default SharedPreferences, make an object
that extends
DefaultPreferences
instead of Preferences
.
Note that for better encapsulation, you might want to keep the mutable delegated properties private in some cases, and expose functions and flows instead.
Unless you use coroutines (read more about this in next section just below),
a class
instead of an object
is not recommended because it would mean
you can instantiate it multiple times, while the underlying preferences
xml file is cached for the rest of your app's process lifetime once loaded,
so in a class
you'd be allocating the delegates more times than needed,
leading to an additional, unneeded, small pressure on the garbage collector.
However, you may make an abstract subclass of Preferences
for specific
use cases where adding logic to base Preferences
or sharing some
properties may be desirable. (If you do, please open an issue to tell us
about this use case. It may become an example shown here.)
Note that this feature is currently only supported on Android. Feel free to open an issue if you want it on other platforms.
The object
approach described above has several advantages, one of
the most significant being ease of use anywhere in your app, but that
also means you can easily access it from the main thread, and the first
time you access the object, the underlying xml file where the preferences
are stored is loaded, which may block the main thread for longer that you
would want, possibly dropping a few frames.
With coroutines, it's easy to offload something on another thread, and this split embraces this capability.
Let's see a modified version of the GamePreferences
described above,
before passing in review each change.
import splitties.preferences.SuspendPrefsAccessor
import splitties.preferences.Preferences
class GamePreferences private constructor() : Preferences("gameState") {
companion object : SuspendPrefsAccessor<GamePreferences>(::GamePreferences)
var magicNumber by intPref(0) // The property name is used as the key.
var currentLevel by IntPref("currentLevel", 1)
var bossesFought by IntPref("bossBattleVictories", 0)
var lastTimePlayed by LongPref("lastSessionTime", 0L)
var pseudo by StringPref("playerPseudo", "Player 1")
var favoriteCharacter by stringOrNullPref()
}
Here are all the changes:
- We moved from
object
toclass
. - We added a
private constructor()
. - We added a
companion object
that extends theSuspendPrefsAccessor
abstract class and calls its constructor with a reference to the constructor.
With this change, we can no longer access the GamePreferences
singleton directly
from anywhere… unless we are in a coroutine!
From any suspend
function, you
just have to call GamePreferences()
like you were calling a constructor, but
in reality, it is a function call that suspends while loading the preferences
for the first time in process life in Dispatchers.IO
.
If the preferences have already been loaded, it immediately returns the now instantiated singleton.
If you have non suspending functions that would need to access the preferences,
you have two options: pass your Preferences
subclass as a parameter, or make
it a suspend
function.
implementation("com.louiscad.splitties:splitties-preferences:$splitties_version")