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 1.0 - Unit testing framework for Java
- AndroidX JUnit - Apache 2.0 - AndroidX JUnit extensions
- Espresso Core - Apache 2.0 - Android UI testing framework
- AndroidX Lifecycle Runtime KTX - Apache 2.0 - Lifecycle-aware components
- AndroidX Lifecycle Runtime Compose - Apache 2.0 - Lifecycle integration with Jetpack Compose
- AndroidX Activity Compose - Apache 2.0 - Activity integration for Jetpack Compose
- Jetpack Compose BOM - Apache 2.0 - Compose Bill of Materials
- AndroidX JUnit KTX - Apache 2.0 - Kotlin extensions for JUnit
- AndroidX Core KTX - Apache 2.0 - Kotlin extensions for core Android libraries
- AndroidX Test Core KTX - Apache 2.0 - Core testing utilities for AndroidX
- Jetpack Compose UI - Apache 2.0 - Compose UI components
- Jetpack Compose UI Graphics - Apache 2.0 - Compose graphics utilities
- Jetpack Compose UI Tooling - Apache 2.0 - Tooling support for Compose UI
- Jetpack Compose UI Tooling Preview - Apache 2.0 - Preview support for Compose
- Jetpack Compose UI Test Manifest - Apache 2.0 - Manifest support for Compose UI testing
- Jetpack Compose UI Test JUnit4 - Apache 2.0 - Compose UI testing with JUnit4
- Material3 - Apache 2.0 - Jetpack Compose Material3 components
- Kotlinx Coroutines Test - Apache 2.0 - Coroutine testing utilities
- AndroidX DataStore Preferences - Apache 2.0 - Data storage solution
- MockK Android - Apache 2.0 - Mocking library for Kotlin
- Robolectric - Apache 2.0 - Unit testing framework for Android
- Timber - Apache 2.0 - Logging utility for Android
- AndroidX Test Rules - Apache 2.0 - JUnit test rules for Android testing
- Hilt Android - Apache 2.0 - Dependency injection for Android
- Hilt Compiler - Apache 2.0 - Annotation processor for Hilt
- Hilt Android Testing - Apache 2.0 - Instrumented testing support for Hilt
- Hilt Navigation Compose - Apache 2.0 - Hilt support for Compose Navigation
- Android Application Plugin - Google - Plugin for building Android applications
- Kotlin Android Plugin - JetBrains - Kotlin support for Android
- Compose Compiler Plugin - JetBrains - Compiler plugin for Jetpack Compose
- Hilt Android Plugin - Google - Plugin for setting up Hilt in Android projects
- KSP Plugin - Google - Kotlin Symbol Processing plugin
- Detekt Plugin - Artur Bosch - Static code analysis for Kotlin
- Kotlinter Plugin - Jeremy Lelliott - Linter for Kotlin