17

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?

fafcrumb
  • 333
  • 3
  • 13
  • Maybe this helps you: https://medium.com/swlh/cool-new-android-apis-jetpack-datastore-e3c32b577476 – Dr.jacky Feb 08 '21 at 11:19
  • 2
    This is such a bummer that for now I'm just going to stick to the old Shared Preferences. The old API still works fine, is not deprecated, and saves me toms of time and codebase to write the preference view and linking it to a backend store. – Constructor Jan 30 '22 at 12:54

3 Answers3

9

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.

asuras
  • 123
  • 7
  • 3
    This is so stupid. Why would Google not build in a way to use DataStore with Preferences? – Kris B Jan 19 '22 at 14:43
  • Gist for a full PreferenceDataStore implementation: https://gist.github.com/Pittvandewitt/c7c620412ec246e34824647a3a4605d0 – Thomas W. Apr 21 '23 at 08:29
5

I have run into the same issue using DataStore. Not only does DataStore not implement PreferenceDataStore, but I believe it is impossible to write an adapter to bridge the two, because the DataStore uses Kotlin Flows and is asynchronous, whereas PreferenceDataStore assumes 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, ConcatAdapter made it much easier, as I can basically create one adapter for each preference item, and then combine them into one adapter using ConcatAdapter.

What I ended up with is a PreferenceItemAdapter that has mutable title, summary, visible, and enabled properties that mimics the behavior of the preference library, and also a Jetpack Compose inspired API that looks like this:

preferenceGroup {
  preference {
    title("Name")
    summary(datastore.data.map { it.name })
    onClick {
      showDialog {
        val text = editText(datastore.data.first().name)
        negativeButton()
        positiveButton()
          .onEach { dataStore.edit { settings -> settings.name = text.first } }.launchIn(lifecycleScope)
      }
    }
  }
  preference {
    title("Date")
    summary(datastore.data.map { it.parsedDate?.format(dateFormatter) ?: "Not configured" })
    onClick {
      showDatePickerDialog(datastore.data.first().parsedDate ?: LocalDate.now()) { newDate ->
        lifecycleScope.launch { dataStore.edit { settings -> settings.date = newDate } }
      }
    }
  }
}

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).

Maurice Lam
  • 1,577
  • 9
  • 14
  • Yep, noticed that same problem when I tried to implement `PreferenceDataStore` but forgot to update here. I'll accept this since it makes clear that it can't be done. – fafcrumb Jan 15 '21 at 19:28
  • 4
    This is ridiculous. Google needs to migrate Preferences to use the new DataStore API. This shouldn't be put on devs. – Kris B Aug 24 '21 at 14:48
1

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.

fafcrumb
  • 333
  • 3
  • 13