I had previously replaced SharedPreferences in my app with the new DataStore, as recommended by Google in the docs, to reap some of the obvious benefits. Then it came time to add a settings screen, and I found the Preferences Library. The confusion came when I saw the library uses SharedPreferences by default with no option to switch to DataStore. You can use setPreferenceDataStore to provide a custom storage implementation, but DataStore does not implement the PreferenceDataStore interface, leaving it up to the developer. And yes this naming is also extremely confusing. I became more confused when I found no articles or questions talking about using DataStore with the Preferences Library, so I feel like I'm missing something. Are people using both of these storage solutions side by side? Or one or the other? If I were to implement PreferenceDataStore in DataStore, are there any gotchas/pitfalls I should be looking out for?
Androidx Preferences Library vs DataStore preferences
3.5k views Asked by fafcrumb AtThere are 3 answers
On
I'll add my own strategy I went with for working around the incompatibility in case it's useful to some:
I stuck with the preference library and added android:persistent="false" to all my editable preferences so they wouldn't use SharedPreferences at all. Then I was free to just save and load the preference values reactively. Storing them through click/change listeners → view model → repository, and reflecting them back with observers.
Definitely messier than a good custom solution, but it worked well for my small app.
On
For anyone reading the question and thinking about the setPreferenceDataStore-solution. Implementing your own PreferencesDataStore with the DataStore instead of SharedPreferences is straight forward at a glance.
class SettingsDataStore(private val dataStore: DataStore<Preferences>): PreferenceDataStore() {
override fun putString(key: String, value: String?) {
CoroutineScope(Dispatchers.IO).launch {
dataStore.edit { it[stringPreferencesKey(key)] = value!! }
}
}
override fun getString(key: String, defValue: String?): String {
return runBlocking { dataStore.data.map { it[stringPreferencesKey(key)] ?: defValue!! }.first() }
}
...
}
And then setting the datastore in your fragment
@AndroidEntryPoint
class AppSettingsFragment : PreferenceFragmentCompat() {
@Inject
lateinit var dataStore: DataStore<Preferences>
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
preferenceManager.preferenceDataStore = SettingsDataStore(dataStore)
setPreferencesFromResource(R.xml.app_preferences, rootKey)
}
}
But there are a few issues with this solution. According to the documentation runBlocking with first() to synchronously read values is the preferred way, but should be used with caution.
Make sure to setpreferenceDataStore before calling setPreferencesFromResource to avoid loading issues where the default implementation (sharedPreferences) will be used for initial loading.
A couple weeks ago on my initial try to implement the PreferenceDataStore, I had troubles with type long keys. My settings screen was correctly showing and saving numeric values for an EditTextPreference but the flows did not emit any values for these keys. There might be an issue with EditTextPreference saving numbers as strings because setting an inputType in the xml seems to have no effect (at least not on the input keyboard). While saving numbers as strings might work, this also requires reading numbers as strings. Therefore you lose the type-safety for primitive types.
Maybe with one or two updates on the settings and datastore libs there might be an official working solution for this case.
I have run into the same issue using DataStore. Not only does
DataStorenot implementPreferenceDataStore, but I believe it is impossible to write an adapter to bridge the two, because theDataStoreuses KotlinFlows and is asynchronous, whereasPreferenceDataStoreassumes that both get and put operations to be synchronous.My solution to this is to write the preference screen manually using a recycler view. Fortunately,
ConcatAdaptermade it much easier, as I can basically create one adapter for each preference item, and then combine them into one adapter usingConcatAdapter.What I ended up with is a
PreferenceItemAdapterthat has mutabletitle,summary,visible, andenabledproperties that mimics the behavior of the preference library, and also a Jetpack Compose inspired API that looks like this:There is more manual code in this approach, but I find it easier than trying to bend the preference library to my will, and gives me the flexibility I needed for my project (which also stores some of the preferences in Firebase).