关于Preferences DataStore的全部内容
在这篇文章中,我们将看看Preferences DataStore,这是两个DataStore实现中的一个。我们将介绍如何创建它、读写数据以及如何处理异常,希望所有这些都能为你提供足够的信息来决定它是否是你应用程序的正确选择。
Preferences DataStore使用键值对来存储较小的数据集,而无需预先定义模式。这可能会让你想起SharedPreferences
,但只是在它结构你的数据模型的方式。与其SharedPreferences
的前身相比,DataStore有多种好处。请随时快速跳回我们的上一篇文章,看看我们在那里做的详细比较。今后,除非另有说明,我们将把Preferences DataStore
称为Preferences
。
快速回顾一下吧。
- 利用Kotlin coroutines的力量,为检索和保存数据提供了一个完全异步的API。
- 不提供随时可用的同步支持—它直接避免了做任何阻塞UI线程的工作
- 依靠Flow的内部错误信号机制,允许你在读或写数据时安全地捕捉和处理异常。
- 在原子的读-修改-写操作中处理数据更新,提供强大的ACID保证
- 允许轻松快速的数据迁移。
- 希望通过最少的更改快速迁移从
SharedPreferences
并在没有完全类型安全的情况下感到足够自信?选择 Preferences 而不是 Proto
现在让我们深入研究一些代码,了解应该如何实现首选项的问题。
我们将使用Preferences DataStore代码实验室的代码。如果你对更多的实践方法感兴趣,我们真的鼓励你自己去看一下与Preferences DataStore合作代码实验室。
这个示例应用程序显示了一个任务列表,用户可以选择按完成状态过滤它们,或按优先级和截止日期排序。我们希望在DataStore中存储他们的选择— 已完成任务的boolean和排序枚举。
依赖性的设置
让我们从添加必要的依赖性开始吧。
/* Copyright 2022 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
implementation "androidx.datastore:datastore-preferences:1.0.0"
💡快速提示—如果你想对你的构建进行最小化,请确保在你的proguard-rules.pro
文件中添加一个额外的规则,以防止你的字段被删除。
/* Copyright 2022 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
<fields>;
}
创建一个首选项数据存储
你通过DataStore<Preferences>
的实例与保存在DataStore中的数据互动。DataStore
是一个接口,它允许对持久化的信息进行访问。Preferences是一个类似于通用地图的抽象类,专门用于DataStore的Preferences实现中,以跟踪数据的键值对。我们将在讨论写入数据时谈论其MutablePreferences
子类。
为了创建这个实例,建议使用委托preferencesDataStore
并传递一个强制性的name
参数。这个委托是一个Kotlin扩展属性,其接收者类型必须是Context
的实例,随后需要构建File
对象,DataStore存储数据。
/* Copyright 2022 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
private val Context.dataStore by preferencesDataStore(
name = USER_PREFERENCES_NAME
)
您不应为给定文件创建多个 DataStore 实例,这样做会破坏所有 DataStore 功能。 因此,您可以在 Kotlin 文件的顶层添加一次委托构造并在整个应用程序中使用它,以便将其作为单例传递。在后面的文章中,我们将看到如何使用依赖注入来做到这一点。
定义键
DataStore提供了一种快速构建不同数据类型的键的方法,如booleanPreferencesKey
、intPreferencesKey
和更多&mdash;你只需要将键的名称作为值传递。虽然这确实给数据类型带来了一些约束,但请记住,它并没有提供明确的类型安全。通过指定某种类型的偏好键,我们希望得到最好的结果,并依赖于我们的假设:某种类型的值会被返回。如果你觉得你的代码的结构可以安全地处理这个问题,可以继续使用偏好。如果没有,请考虑使用Preferences’兄弟姐妹,Proto DataStore,因为它提供了完整的类型安全。
在我们的应用程序的UserPreferencesRepository
中,我们指定了结构化我们持久化数据的键值对所需的所有键值。
/* Copyright 2022 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
private object PreferencesKeys {
val SORT_ORDER = stringPreferencesKey("sort_order")
val SHOW_COMPLETED = booleanPreferencesKey("show_completed")
}
读取数据
为了读取存储的数据,在UserPreferencesRepository
中,我们从dataStore.data
中暴露了一个Flow<Preferences>
。这提供了对最新保存状态的有效访问,并在每次变化时排放。使用Kotlin数据类,我们可以观察任何排放,并将提供的DataStore Preferences
对象转化为我们自己的UserPreferences
模型,只使用我们’感兴趣的键值对。
/* Copyright 2022 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
val userPreferencesFlow: Flow<UserPreferences> = dataStore.data.map { preferences ->
val sortOrder = SortOrder.valueOf(preferences[PreferencesKeys.SORT_ORDER] ?: SortOrder.NONE.name)
val showCompleted = preferences[PreferencesKeys.SHOW_COMPLETED] ?: false
UserPreferences(showCompleted, sortOrder)
}
当试图从磁盘上读取数据时,Flow总是会发出一个值或者抛出一个异常。我们将在后面的章节中讨论异常处理。DataStore还确保工作总是在Dispatchers.IO
上进行,这样你的UI线程就不会被阻塞。
🚨不要创建任何缓存库来反映你的首选项数据的当前状态。这样做会使DataStore’数据一致性的保证失效。如果你需要你的数据的单一快照,而不需要订阅进一步的Flow排放,最好使用dataStore.data.first()
。
/* Copyright 2022 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
// Don't
suspend fun fetchCachedPrefs(scope: CoroutineScope): StateFlow<Preferences> = dataStore.data.stateIn(scope)
// Do
suspend fun fetchInitialPreferences() = mapUserPreferences(dataStore.data.first().toPreferences())
写入数据
对于写入数据,我们将使用一个暂停DataStore<Preferences>.edit(transform: suspend (MutablePreferences) -> Unit)
函数。让我们把它分解一下。
DataStore<Preferences>
接口&mdash;我们目前使用datastore作为具体的Preferences
实现。transform: suspend (MutablePreferences) -> Unit
— 一个暂停块,用于将指定的变化应用到我们的持久化数据中。MutablePreferences
—Preferences
的一个可变子类,类似于MutableMap
,它允许我们对我们的键值对进行修改。
作为一个例子,我们将改变我们的SHOW_COMPLETED
旗帜。
/* Copyright 2022 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
suspend fun updateShowCompleted(showCompleted: Boolean) {
dataStore.edit { preferences ->
preferences[PreferencesKeys.SHOW_COMPLETED] = showCompleted
}
}
编辑数据是在原子的读-修改-写操作中事务性完成的。这意味着数据处理操作的特定顺序,在此期间,数据被锁定给其他线程,保证了一致性和防止竞赛条件。只有在transform
和edit
coroutines成功完成后,数据才会被持久地保存在磁盘上,datastore.data
Flow才会反映出更新。
🚨请记住,这是对DataStore状态进行修改的唯一方法。保留一个MutablePreferences
的引用并在transform
完成后手动改变它,将不会改变DataStore中的持久化数据,所以你不应该试图在transform
块之外修改MutablePreferences
。
如果写入操作因任何原因而失败,事务就会被中止,并抛出一个异常。
从SharedPreferences中迁移出来
如果你’之前在你的应用程序中使用了SharedPreferences
,并希望将其数据安全地转移到首选项,你可以使用SharedPreferencesMigration
。它需要一个上下文、SharedPreferences
名称和一组你希望迁移的可选键(或者直接将其作为默认的MIGRATE_ALL_KEYS
值)。
查看SharedPreferencesMigration
实现,你会看到一个getMigrationFunction()
,它负责获取所有需要的、存储的键值对,然后使用相同的键值将它们添加到首选项。通过preferencesDataStore
委托的produceMigrations
参数传递SharedPreferencesMigration
,以轻松迁移。
/* Copyright 2022 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
private val Context.dataStore by preferencesDataStore(
name = USER_PREFERENCES_NAME,
produceMigrations = { context ->
listOf(SharedPreferencesMigration(context, USER_PREFERENCES_NAME))
}
)
produceMigrations
将确保getMigrationFunction()
在在任何潜在的数据访问 到DataStore 之前运行。这意味着您的迁移必须在 DataStore 发出任何进一步的值之前以及开始对数据进行任何新更改之前成功。成功迁移后,停止使用SharedPreferences
是安全的,因为密钥仅迁移一次,然后从SharedPreferences
删除。
produceMigrations
接受一个DataMigration
的列表。我们将在后面的章节中看到我们如何将其用于其他类型的数据迁移。如果你不需要迁移,你可以忽略这个,因为它已经有一个默认的listOf()
提供了。
异常情况的处理
与SharedPreferences
相比,DataStore的主要优势之一是其捕获和处理异常的成熟的机制。虽然SharedPreferences
将解析错误作为运行时异常抛出,为意外的、未捕获的崩溃留下了空间,但DataStore在读/写数据发生错误时抛出一个IOException
。
我们可以通过在map()
之前使用catch()
流量运算符,并发出emptyPreferences()
来安全地处理这个问题。
/* Copyright 2022 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
val userPreferencesFlow: Flow<UserPreferences> = dataStore.data.catch { exception ->
// dataStore.data throws an IOException when an error is encountered when reading data
if (exception is IOException) {
Log.e(TAG, "Error reading preferences.", exception)
emit(emptyPreferences())
} else {
throw exception
}
}.map { preferences ->
mapUserPreferences(preferences)
}
或者用一个简单的try-catch
块来写。
/* Copyright 2022 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
suspend fun updateShowCompleted(showCompleted: Boolean) {
try {
dataStore.edit { preferences ->
preferences[PreferencesKeys.SHOW_COMPLETED] = showCompleted
}
} catch (e: IOException) {
// Handle error
}
}
如果抛出了不同类型的异常,最好重新抛出它。
待续篇
我们已经介绍了DataStore’的Preferences实现&mdash;何时以及如何使用它来读写数据,如何处理错误以及如何从SharedPreferences
迁移。在下一篇文章中,我们将用Proto DataStore的实现来介绍同样的话题,所以请继续关注。
你可以在这里找到我们的Jetpack DataStore系列的所有文章: Jetpack DataStore简介 关于Preferences DataStore 关于Proto DataStore DataStore和依赖注入 DataStore和Kotlin序列化 DataStore和同步工作 DataStore和数据迁移 DataStore和测试
同样,大多数关于它的文章或视频显示了一个简单的事情。同时,SharedPreferences的使用也被用来创建Preferences屏幕。甚至谷歌自己都没有告诉我们/写一些文档来实现它的偏好。
我不明白。
如果它应该是我们从SharedPreferences迁移到它的东西,那么它是如何与Preference(设置屏幕)一起工作的?
这些都依赖于SharedPreferences...
另外,由于它是异步的,我如何使用它来获取用户在应用程序的设置中选择的主题,例如,在将其设置为活动之前,如何使用它?