This is an experimental project that tries to put the legacy SharedPreferences and the new Jetpack Preferences Datastore side by side by means of dependency inversion to see how it would look if it were to provide the same preferences storage at the data layer.
In my first few Android apps, which date back to 2010, we did not have any architecture to follow. We did not have fragments, so we mostly allocated an activity class for each screen. There, if we wanted to deal with user preferences, it was so easy that we could have placed the code under onResume or even onCreated. SharedPreferences is non-blocking, so it works quickly and simply for most small use cases when it does not break.
- Later, people suggested that SharedPreferences being synchronous can be a problem. That is sensible when developers abuse SharedPreferences by storing a massive amount of key pairs.
- Later, people came up with more different architectures, so we are not simply accessing user preferences right from the activity class.
Eventually, if we want to access user preferences, we can have many more boilerplate codes before executing the few lines that do the work.
Now we have Jetpack Preference Datastore. It is asynchronous, which means that when we want to retrieve user preferences, the result is not immediately available. Up to this moment, if we want to observe preference changes, there is a known limitation: we are not being told which key pair has changed. We know only something has changed, so we probably have to propagate all the keys we are interested in, even if we know that only one has changed.
private val sharedPref = context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
private val onPreferenceChangeListener = SharedPreferences.OnSharedPreferenceChangeListener { sharedPref, key ->
when (key) {
prefKeyString -> {
_stringPreference.value = sharedPref.getString(prefKeyString, null) ?: stringPreferenceDefault
}
prefKeyBoolean -> {
_booleanPreference.value = sharedPref.getBoolean(prefKeyBoolean, booleanPreferenceDefault)
}
}
}
init {
// If we want to keep track of the changes
sharedPref.registerOnSharedPreferenceChangeListener(onPreferenceChangeListener)
}
fun getStringPreference() = sharedPref.getString(prefKeyString, null) ?: "default-value"
fun updateStringPreference(newValue: String) {
try {
sharedPref.edit()
.putString(prefKeyString, newValue)
.apply()
} catch (e: Throwable) {
// Not likely to produce exception though
}
}
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "preferences")
private val prefKeyStrings:Preferences.Key<String> = stringPreferencesKey("some-key-name")
init {
// assume caller passing in Context.dataStore as DataStore<Preferences>
externalCoroutineScope.launch(dispatcher) {
dataStore.data.catch { exception ->
_preferenceErrors.emit(exception)
}
.collect { prefs ->
// or use map
_stringPreference.value = prefs[prefKeyString] ?: stringPreferenceDefault
_booleanPreference.value = prefs[prefKeyBoolean] ?: booleanPreferenceDefault
_intPreference.value = prefs[prefKeyInt] ?: intPreferenceDefault
}
}
}
suspend fun updateStringPreference(newValue: String) {
withContext(dispatcher) {
try {
dataStore.edit { mutablePreferences ->
mutablePreferences[prefKeyString] = newValue
}
} catch (e: Throwable) {
_preferenceErrors.emit(e)
}
}
}
Whether for SharedPreferences or Jetpack Preferences Datastore, even if the core is about 10 lines of code, this code project tries to put them in the right place when following the MVVM and Clean architecture. That means the UI will talk to the ViewModel, which will then connect to a repository that invisibly talks to either the SharedPreferences or the Jetpack Preferences Datastore data store. Dependency inversion with Dagger Hilt allows injecting different data sources ( SharedPreferences and Jetpack Preferences Data Store) into the same repository. Usually in production apps it is not likely that we have a need to use both sources interchangeably.
This project was configured to build using Android Studio Iguana | 2023.2.1. You will need to have Java 17 to build the project.
Alternatively, you can find the ready-to-install APKs and App Bundles under the release section.
- JUnit - EPL 2.0 - A simple framework to write repeatable tests
- AndroidX Test Ext JUnit - Apache 2.0 - Extensions for Android testing
- AndroidX Espresso - Apache 2.0 - UI testing framework
- AndroidX Lifecycle - Apache 2.0 - Lifecycles-aware components
- AndroidX Activity Compose - Apache 2.0 - Jetpack Compose integration with Activity
- Jetpack Compose BOM - Apache 2.0 - Bill of Materials for Jetpack Compose
- AndroidX Core KTX - Apache 2.0 - Extensions to Java APIs for Android development
- AndroidX Compose UI - Apache 2.0 - UI components for Jetpack Compose
- AndroidX Material3 - Apache 2.0 - Material Design components for Jetpack Compose
- JUnit Vintage Engine - EPL 2.0 - Support for running JUnit 3 and JUnit 4 tests
- Kotlinx Coroutines Test - Apache 2.0 - Testing libraries for Kotlin coroutines
- AndroidX DataStore Preferences - Apache 2.0 - Data storage solution
- MockK - Apache 2.0 - Mocking library for Kotlin
- Robolectric - MIT - A framework that brings fast, reliable unit tests to Android
- Timber - Apache 2.0 - A logger with a small, extensible API
- Hilt - Apache 2.0 - A dependency injection library for Android that reduces the boilerplate of doing manual dependency injection
- Android Application Plugin - Google - Plugin for building Android applications
- Jetbrains Kotlin Android Plugin - JetBrains - Plugin for Kotlin Android projects
- Compose Compiler Plugin - JetBrains - Plugin for Jetpack Compose
- Ktlint Plugin - JLLeitschuh - Plugin for Kotlin linter
- Hilt Android Plugin - Google - Plugin for Hilt dependency injection
- Google DevTools KSP - Google - Kotlin Symbol Processing API plugin