Androidx 首选项库与数据存储首选项

Androidx Preferences Library vs DataStore preferences

我之前按照文档中 Google 的建议,用新的 DataStore 替换了我的应用程序中的 SharedPreferences,以获得一些明显的好处。然后是添加设置屏幕的时候了,我找到了首选项库。当我看到该库默认使用 SharedPreferences 而没有切换到 DataStore 的选项时,我感到很困惑。您可以使用 setPreferenceDataStore 提供自定义存储实现,但 DataStore 没有实现 PreferenceDataStore 接口,留给开发人员。是的,这个命名也非常混乱。当我发现没有任何文章或问题谈论将 DataStore 与 Preferences Library 一起使用时,我变得更加困惑,所以我觉得我错过了什么。人们是否同时使用这两种存储解决方案?或者一个或另一个?如果我要在 DataStore 中实现 PreferenceDataStore,有什么我应该注意的 gotchas/pitfalls 吗?

我在使用 DataStore 时遇到了同样的问题 运行。 DataStore 不仅没有实现 PreferenceDataStore,而且我认为不可能编写一个适配器来桥接两者,因为 DataStore 使用 Kotlin Flows 并且是异步的,而 PreferenceDataStore 假设 get 和 put 操作都是同步的。

我的解决方案是使用回收站视图手动编写首选项屏幕。幸运的是,ConcatAdapter 使它变得更容易,因为我基本上可以为每个首选项创建一个适配器,然后使用 ConcatAdapter.

将它们组合成一个适配器

我最终得到的是一个 PreferenceItemAdapter,它具有可变的 titlesummaryvisibleenabled 属性,可以模仿首选项库,以及受 Jetpack Compose 启发的 API,如下所示:

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 } }
      }
    }
  }
}

这种方法中有更多的手动代码,但我发现它比尝试根据我的意愿改变首选项库更容易,并且为我的项目提供了所需的灵活性(它还在 Firebase 中存储了一些首选项).

我将添加我自己的解决不兼容性的策略,以防它对某些人有用:

我坚持使用首选项库并将 android:persistent="false" 添加到我所有可编辑的首选项中,因此他们根本不会使用 SharedPreferences。然后我可以自由地反应性地保存和加载偏好值。通过click/change listeners → view model → repository 存储它们,并用观察者反射回来。

绝对比一个好的自定义解决方案更混乱,但它对我的小应用程序来说效果很好。

对于阅读问题并思考 setPreferenceDataStore 解决方案的任何人。使用 DataStore 而不是 SharedPreferences 实现您自己的 PreferencesDataStore 一目了然。

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() }
    }

    ...
}

然后在您的片段中设置数据存储

@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)
    }
}

但是此解决方案存在一些问题。根据 documentation runBlockingfirst() 同步读取值是首选方式,但应谨慎使用。

确保在调用 setPreferencesFromResource 之前设置preferenceDataStore 以避免加载问题,因为默认实现 (sharedPreferences) 将用于初始加载。

几周前,在我最初尝试实现 PreferenceDataStore 时,我遇到了类型 long 键的问题。我的设置屏幕正确显示并保存了 EditTextPreference 的数值,但流程没有为这些键发出任何值。 EditTextPreference 将数字保存为字符串可能存在问题,因为在 xml 中设置 inputType 似乎无效(至少在输入键盘上无效)。虽然将数字保存为字符串可能有效,但这也需要将数字读取为字符串。因此,您失去了原始类型的类型安全性。

也许对设置和数据存储库进行一两次更新,可能会有针对这种情况的官方工作解决方案。