androidsharedpreferencespreferencescreenandroid-jetpack-datastore

Androidx Preferences Library vs DataStore preferences


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?


Solution

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