1. 简介
什么是 DataStore?
DataStore 是一种新的改进的数据存储解决方案,旨在取代 SharedPreferences。DataStore 基于 Kotlin 协程和 Flow 构建,提供了两种不同的实现:**Proto DataStore**,允许您存储**类型化对象**(由协议缓冲区支持)和**Preferences DataStore**,存储**键值对**。数据以异步、一致和事务的方式存储,克服了 SharedPreferences 的一些缺点。
您将学到什么
- 什么是 DataStore 以及为什么要使用它。
- 如何将 DataStore 添加到您的项目中。
- Preferences 和 Proto DataStore 之间的区别以及各自的优势。
- 如何使用 Proto DataStore。
- 如何从 SharedPreferences 迁移到 Proto DataStore。
您将构建什么
在此代码实验室中,您将从一个示例应用程序开始,该应用程序显示任务列表,这些任务可以按其已完成状态进行筛选,并可以按优先级和截止日期进行排序。
“显示已完成任务”筛选器的布尔标志保存在内存中。排序顺序使用SharedPreferences
对象持久保存到磁盘。
由于 DataStore 有两种不同的实现:Preferences DataStore 和 Proto DataStore,您将学习如何在每种实现中使用**Proto DataStore**完成以下任务
- 将已完成状态筛选器持久保存到 DataStore。
- 将排序顺序从 SharedPreferences 迁移到 DataStore。
我们建议您也学习 Preferences DataStore 代码实验室,以便更好地了解两者之间的区别。
您需要什么
- Android Studio Arctic Fox。
- 熟悉以下架构组件:LiveData、ViewModel、View Binding以及应用架构指南中建议的架构。
- 熟悉协程和 Kotlin Flow。
有关架构组件的介绍,请查看Room with a View 代码实验室。有关 Flow 的介绍,请查看使用 Kotlin Flow 和 LiveData 的高级协程代码实验室。
2. 设置
在此步骤中,您将下载整个代码实验室的代码,然后运行一个简单的示例应用程序。
为了让您尽快开始,我们为您准备了一个启动项目供您构建。
如果您已安装 git,则只需运行以下命令。要检查是否已安装 git,请在终端或命令行中键入git --version
,并验证它是否可以正确执行。
git clone https://github.com/android/codelab-android-datastore
初始状态位于master
分支中。解决方案代码位于proto_datastore
分支中。
如果您没有 git,可以点击以下按钮下载此代码实验室的所有代码
- 解压缩代码,然后在 Android Studio Arctic Fox 中打开项目。
- 在设备或模拟器上运行**app**运行配置。
应用程序运行并显示任务列表
3. 项目概述
该应用程序允许您查看任务列表。每个任务具有以下属性:名称、已完成状态、优先级和截止日期。
为了简化我们需要处理的代码,该应用程序只允许您执行两个操作
- 切换“显示已完成任务”可见性 - 默认情况下隐藏任务
- 按优先级、截止日期或截止日期和优先级对任务进行排序
该应用程序遵循应用架构指南中推荐的架构。以下是您在每个包中将找到的内容
data
Task
模型类。TasksRepository
类 - 负责提供任务。为简单起见,它返回硬编码数据并通过Flow
公开它以表示更真实的场景。UserPreferencesRepository
类 - 保存SortOrder
,定义为enum
。当前的**排序顺序作为String
保存在 SharedPreferences 中**,基于枚举值名称。它公开了同步方法来保存和获取排序顺序。
ui
- 与显示带有
RecyclerView
的Activity
相关的类。 TasksViewModel
类负责 UI 逻辑。
TasksViewModel
- 保存构建 UI 需要显示的数据所需的所有元素:任务列表、showCompleted
和sortOrder
标志,包装在TasksUiModel
对象中。每次这些值之一发生更改时,我们都必须重新构建一个新的TasksUiModel
。为此,我们结合了 3 个元素
- 从
TasksRepository
检索Flow<List<Task>>
。 - 一个
MutableStateFlow<Boolean>
,保存最新的showCompleted
标志,该标志仅保存在**内存中**。 - 一个
MutableStateFlow<SortOrder>
,保存最新的sortOrder
值。
为了确保我们仅在 Activity 启动时正确更新 UI,我们公开了LiveData<TasksUiModel>
。
我们的代码存在一些问题
- 在初始化
UserPreferencesRepository.sortOrder
时,我们阻塞了磁盘 IO 上的 UI 线程。这可能导致 UI 卡顿。 showCompleted
标志仅保存在内存中,这意味着每次用户打开应用程序时它都会重置。与SortOrder
一样,这应该持久化以在关闭应用程序后继续存在。- 我们目前正在使用 SharedPreferences 持久保存数据,但我们在内存中保留了一个
MutableStateFlow
,我们手动修改它以能够收到更改通知。如果值在应用程序的其他地方被修改,这很容易破坏。 - 在
UserPreferencesRepository
中,我们公开了两种更新排序顺序的方法:enableSortByDeadline()
和enableSortByPriority()
。这两种方法都依赖于当前的排序顺序值,但是,如果一个方法在另一个方法完成之前被调用,我们最终会得到错误的最终值。更重要的是,这些方法可能导致 UI 卡顿和 Strict Mode 违规,因为它们是在 UI 线程上调用的。
尽管showCompleted
和sortOrder
标志都是用户首选项,但它们目前表示为两个不同的对象。因此,我们的目标之一是将这两个标志统一到UserPreferences
类下。
让我们了解如何使用 DataStore 来帮助我们解决这些问题。
4. DataStore - 基础知识
通常,您可能会发现自己需要存储少量或简单的数据集。为此,过去您可能使用了 SharedPreferences,但此 API 也有一些缺点。Jetpack DataStore 库旨在解决这些问题,为存储数据创建一个简单、更安全和异步的 API。它提供了 2 种不同的实现
- Preferences DataStore
- Proto DataStore
功能 | SharedPreferences | PreferencesDataStore | ProtoDataStore |
异步 API | ✅(仅用于读取更改的值,通过侦听器) | ✅(通过 | ✅(通过 |
同步 API | ✅(但不安全在 UI 线程上调用) | ❌ | ❌ |
安全在 UI 线程上调用 | ❌(1) | ✅(工作在后台移至 | ✅(工作在后台移至 |
可以发出错误信号 | ❌ | ✅ | ✅ |
免受运行时异常 | ❌(2) | ✅ | ✅ |
具有具有强一致性保证的事务 API | ❌ | ✅ | ✅ |
处理数据迁移 | ❌ | ✅ | ✅ |
类型安全 | ❌ | ❌ | ✅ 使用协议缓冲区 |
(1) SharedPreferences 具有一个同步 API,在 UI 线程上调用时看起来很安全,但实际上它执行磁盘 I/O 操作。此外,apply()
在fsync()
上阻塞 UI 线程。每次任何服务启动或停止以及每次应用程序中任何位置的活动启动或停止时,都会触发挂起的fsync()
调用。UI 线程被apply()
调度的挂起的fsync()
调用阻塞,通常成为ANR的来源。
(2) SharedPreferences 将解析错误作为运行时异常抛出。
Preferences 与 Proto DataStore
虽然 Preferences 和 Proto DataStore 都允许保存数据,但它们以不同的方式执行此操作
- **Preference DataStore**,像 SharedPreferences 一样,基于键访问数据,而无需预先定义模式。
- **Proto DataStore**使用协议缓冲区定义模式。使用 Protobufs 允许**持久化强类型数据**。它们比 XML 和其他类似的数据格式更快、更小、更简单且更不容易产生歧义。虽然 Proto DataStore 要求您学习一种新的序列化机制,但我们认为 Proto DataStore 带来的强类型优势是值得的。
Room 与 DataStore
如果您需要部分更新、引用完整性或大型/复杂数据集,则应考虑使用 Room 而不是 DataStore。DataStore 非常适合小型或简单的数据集,并且不支持部分更新或引用完整性。
5. Proto DataStore - 概述
SharedPreferences 和 Preferences DataStore 的缺点之一是无法定义模式或确保使用正确的类型访问键。Proto DataStore 通过使用协议缓冲区来定义模式来解决此问题。使用 protos DataStore 知道存储了哪些类型,并且只会提供它们,从而无需使用键。
让我们看看如何将 Proto DataStore 和 Protobufs 添加到项目中,协议缓冲区是什么以及如何将它们与 Proto DataStore 一起使用,以及如何将 SharedPreferences 迁移到 DataStore。
添加依赖项
要使用 Proto DataStore 并让 Protobuf 为我们的模式生成代码,我们需要对模块的 build.gradle 文件进行一些更改
- 添加 Protobuf 插件
- 添加 Protobuf 和 Proto DataStore 依赖项
- 配置 Protobuf
plugins {
...
id "com.google.protobuf" version "0.8.17"
}
dependencies {
implementation "androidx.datastore:datastore:1.0.0"
implementation "com.google.protobuf:protobuf-javalite:3.18.0"
...
}
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:21.7"
}
// Generates the java Protobuf-lite code for the Protobufs in this project. See
// https://github.com/google/protobuf-gradle-plugin#customizing-protobuf-compilation
// for more information.
generateProtoTasks {
all().each { task ->
task.builtins {
java {
option 'lite'
}
}
}
}
}
6. 定义和使用 protobuf 对象
Protocol buffers 是一种用于序列化结构化数据的机制。您只需定义一次希望数据如何结构化,然后编译器就会生成源代码,以便轻松地写入和读取结构化数据。
创建 proto 文件
您在 proto 文件中定义模式。在我们的 codelab 中,我们有两个用户偏好:show_completed
和 sort_order
;目前它们以两个不同的对象表示。因此,我们的目标之一是将这两个标志统一到存储在 DataStore 中的 UserPreferences
类下。我们不会在 Kotlin 中定义此类,而是在 protobuf 模式中定义它。
查看Proto 语言指南,以获取有关语法的详细信息。在本 codelab 中,我们只关注所需的类型。
在 app/src/main/proto
目录中创建一个名为 user_prefs.proto
的新文件。如果您没有看到此文件夹结构,请切换到项目视图。在 protobuf 中,每个结构都使用 message
关键字定义,结构的每个成员都在消息内部定义,基于类型和名称,并分配一个从 1 开始的顺序。让我们定义一个 UserPreferences
消息,目前它只包含一个名为 show_completed
的布尔值。
syntax = "proto3";
option java_package = "com.codelab.android.datastore";
option java_multiple_files = true;
message UserPreferences {
// filter for showing / hiding completed tasks
bool show_completed = 1;
}
创建序列化器
为了告诉 DataStore 如何读取和写入我们在 proto 文件中定义的数据类型,我们需要实现一个序列化器。序列化器还定义了如果磁盘上没有数据则返回的默认值。在 data
包中创建一个名为 UserPreferencesSerializer
的新文件。
object UserPreferencesSerializer : Serializer<UserPreferences> {
override val defaultValue: UserPreferences = UserPreferences.getDefaultInstance()
override suspend fun readFrom(input: InputStream): UserPreferences {
try {
return UserPreferences.parseFrom(input)
} catch (exception: InvalidProtocolBufferException) {
throw CorruptionException("Cannot read proto.", exception)
}
}
override suspend fun writeTo(t: UserPreferences, output: OutputStream) = t.writeTo(output)
}
7. 在 Proto DataStore 中持久化数据
创建 DataStore
showCompleted
标志保存在内存中,在 TasksViewModel
中,但它应该存储在 UserPreferencesRepository
中,在一个 DataStore 实例中。
要创建 DataStore 实例,我们使用 dataStore
代理,以 Context
作为接收者。该代理有两个必填参数
- DataStore 将作用于的文件的名称。
- 用于 DataStore 的类型的序列化器。在我们的例子中:
UserPreferencesSerializer
。
为简单起见,在本 codelab 中,让我们在 TasksActivity.kt
中执行此操作。
private const val USER_PREFERENCES_NAME = "user_preferences"
private const val DATA_STORE_FILE_NAME = "user_prefs.pb"
private const val SORT_ORDER_KEY = "sort_order"
private val Context.userPreferencesStore: DataStore<UserPreferences> by dataStore(
fileName = DATA_STORE_FILE_NAME,
serializer = UserPreferencesSerializer
)
class TasksActivity: AppCompatActivity() { ... }
dataStore
代理确保我们在应用程序中只有一个名为该名称的 DataStore 实例。目前,UserPreferencesRepository
实现为单例,因为它保存了 sortOrderFlow
并避免将其绑定到 TasksActivity
的生命周期。因为 UserPreferenceRepository
只会处理来自 DataStore 的数据,并且不会创建和保存任何新对象,所以我们现在就可以删除单例实现。
- 删除
companion object
- 将
constructor
设置为 public
UserPreferencesRepository
应该获取一个 DataStore
实例作为构造函数参数。目前,我们可以将 Context
作为参数保留,因为它被 SharedPreferences 需要,但我们稍后会将其删除。
class UserPreferencesRepository(
private val userPreferencesStore: DataStore<UserPreferences>,
context: Context
) { ... }
让我们更新 TasksActivity
中 UserPreferencesRepository
的构造并传入 dataStore
viewModel = ViewModelProvider(
this,
TasksViewModelFactory(
TasksRepository,
UserPreferencesRepository(userPreferencesStore, this)
)
).get(TasksViewModel::class.java)
从 Proto DataStore 读取数据
Proto DataStore 在 Flow<UserPreferences>
中公开存储的数据。让我们创建一个公共的 userPreferencesFlow: Flow<UserPreferences>
值,该值被分配给 dataStore.data
val userPreferencesFlow: Flow<UserPreferences> = userPreferencesStore.data
处理读取数据时的异常
由于 DataStore 从文件中读取数据,因此在读取数据时发生错误时会抛出 IOException
。我们可以使用 catch
Flow 变换来处理这些异常,并只记录错误。
private val TAG: String = "UserPreferencesRepo"
val userPreferencesFlow: Flow<UserPreferences> = userPreferencesStore.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 sort order preferences.", exception)
emit(UserPreferences.getDefaultInstance())
} else {
throw exception
}
}
将数据写入 Proto DataStore
要写入数据,DataStore 提供了一个挂起函数 DataStore.updateData()
,其中我们获取 UserPreferences
的当前状态作为参数。要更新它,我们将必须将首选项对象转换为构建器,设置新值,然后构建新的首选项。
updateData()
以原子读写修改操作的方式更新数据。一旦数据持久化到磁盘上,协程就会完成。
让我们创建一个挂起函数,允许我们更新 UserPreferences
的 showCompleted
属性,称为 updateShowCompleted()
,它调用 dataStore.updateData()
并设置新值。
suspend fun updateShowCompleted(completed: Boolean) {
userPreferencesStore.updateData { preferences ->
preferences.toBuilder().setShowCompleted(completed).build()
}
}
此时,应用程序应该可以编译,但我们刚刚在 UserPreferencesRepository
中创建的功能没有被使用。
8. 从 SharedPreferences 到 Proto DataStore
定义要保存在 proto 中的数据
排序顺序保存在 SharedPreferences 中。让我们将其移动到 DataStore。为此,让我们首先更新 proto 文件中的 UserPreferences
以存储排序顺序。由于 SortOrder
是一个 enum
,我们将不得不将其定义在我们的 UserPreference
中。enums
在 protobuf 中的定义方式类似于 Kotlin。
对于枚举,默认值为枚举类型定义中列出的第一个值。但是,当从 SharedPreferences 迁移时,我们需要知道我们获取的值是默认值还是之前在 SharedPreferences 中设置的值。为了帮助解决这个问题,我们为 SortOrder
枚举定义了一个新值:UNSPECIFIED
,并将其列在第一位,以便它可以成为默认值。
我们的 user_prefs.proto
文件应该如下所示
syntax = "proto3";
option java_package = "com.codelab.android.datastore";
option java_multiple_files = true;
message UserPreferences {
// filter for showing / hiding completed tasks
bool show_completed = 1;
// defines tasks sorting order: no order, by deadline, by priority, by deadline and priority
enum SortOrder {
UNSPECIFIED = 0;
NONE = 1;
BY_DEADLINE = 2;
BY_PRIORITY = 3;
BY_DEADLINE_AND_PRIORITY = 4;
}
// user selected tasks sorting order
SortOrder sort_order = 2;
}
清理并重建您的项目,以确保生成了一个新的 UserPreferences
对象,其中包含新字段。
现在 SortOrder
在 proto 文件中已定义,我们可以从 UserPreferencesRepository
中删除该声明。删除
enum class SortOrder {
NONE,
BY_DEADLINE,
BY_PRIORITY,
BY_DEADLINE_AND_PRIORITY
}
确保在任何地方都使用了正确的 SortOrder
导入
import com.codelab.android.datastore.UserPreferences.SortOrder
在 TasksViewModel.filterSortTasks()
中,我们根据 SortOrder
类型执行不同的操作。现在我们还添加了 UNSPECIFIED
选项,我们需要为 when(sortOrder)
语句添加另一个情况。因为我们不想处理除我们现在正在处理的选项之外的其他选项,所以我们可以在其他情况下抛出 UnsupportedOperationException
。
我们的 filterSortTasks()
函数现在如下所示
private fun filterSortTasks(
tasks: List<Task>,
showCompleted: Boolean,
sortOrder: SortOrder
): List<Task> {
// filter the tasks
val filteredTasks = if (showCompleted) {
tasks
} else {
tasks.filter { !it.completed }
}
// sort the tasks
return when (sortOrder) {
SortOrder.UNSPECIFIED -> filteredTasks
SortOrder.NONE -> filteredTasks
SortOrder.BY_DEADLINE -> filteredTasks.sortedByDescending { it.deadline }
SortOrder.BY_PRIORITY -> filteredTasks.sortedBy { it.priority }
SortOrder.BY_DEADLINE_AND_PRIORITY -> filteredTasks.sortedWith(
compareByDescending<Task> { it.deadline }.thenBy { it.priority }
)
// We shouldn't get any other values
else -> throw UnsupportedOperationException("$sortOrder not supported")
}
}
从 SharedPreferences 迁移
为了帮助迁移,DataStore 定义了 SharedPreferencesMigration
类。by dataStore
方法创建 DataStore(在 TasksActivity
中使用),还公开了 produceMigrations
参数。在此块中,我们创建了应为此 DataStore 实例运行的 DataMigration
列表。在我们的例子中,我们只有一个迁移:SharedPreferencesMigration
。
在实现 SharedPreferencesMigration
时,migrate
块会给我们两个参数
SharedPreferencesView
允许我们从 SharedPreferences 中检索数据UserPreferences
当前数据
我们将必须返回一个 UserPreferences
对象。
在实现 migrate
块时,我们将必须执行以下步骤
- 检查
UserPreferences
中的sortOrder
值。 - 如果这是
SortOrder.UNSPECIFIED
,则表示我们需要从 SharedPreferences 中检索值。如果缺少SortOrder
,则可以使用SortOrder.NONE
作为默认值。 - 获取排序顺序后,我们将必须将
UserPreferences
对象转换为构建器,设置排序顺序,然后通过调用build()
再次构建对象。此更改不会影响其他字段。 - 如果
UserPreferences
中的sortOrder
值不是SortOrder.UNSPECIFIED
,我们可以直接返回我们在migrate
中获取的当前数据,因为迁移必须已经成功运行。
private val sharedPrefsMigration = SharedPreferencesMigration(
context,
USER_PREFERENCES_NAME
) { sharedPrefs: SharedPreferencesView, currentData: UserPreferences ->
// Define the mapping from SharedPreferences to UserPreferences
if (currentData.sortOrder == SortOrder.UNSPECIFIED) {
currentData.toBuilder().setSortOrder(
SortOrder.valueOf(
sharedPrefs.getString(SORT_ORDER_KEY, SortOrder.NONE.name)!!
)
).build()
} else {
currentData
}
}
现在我们定义了迁移逻辑,我们需要告诉 DataStore 它应该使用它。为此,更新 DataStore 构建器并将 migrations
参数分配给一个包含我们 SharedPreferencesMigration
实例的新列表。
private val userPreferencesStore: DataStore<UserPreferences> = context.createDataStore(
fileName = "user_prefs.pb",
serializer = UserPreferencesSerializer,
migrations = listOf(sharedPrefsMigration)
)
将排序顺序保存到 DataStore
要更新调用 enableSortByDeadline()
和 enableSortByPriority()
时的排序顺序,我们必须执行以下操作
- 在
dataStore.updateData()
的 lambda 中调用它们各自的功能。 - 由于
updateData()
是一个挂起函数,因此enableSortByDeadline()
和enableSortByPriority()
也应该设置为挂起函数。 - 使用从
updateData()
接收到的当前UserPreferences
来构造新的排序顺序。 - 通过将其转换为构建器,设置新的排序顺序,然后再次构建首选项来更新
UserPreferences
。
以下是 enableSortByDeadline()
的实现方式。我们将让您自己对 enableSortByPriority()
进行更改。
suspend fun enableSortByDeadline(enable: Boolean) {
// updateData handles data transactionally, ensuring that if the sort is updated at the same
// time from another thread, we won't have conflicts
dataStore.updateData { preferences ->
val currentOrder = preferences.sortOrder
val newSortOrder =
if (enable) {
if (currentOrder == SortOrder.BY_PRIORITY) {
SortOrder.BY_DEADLINE_AND_PRIORITY
} else {
SortOrder.BY_DEADLINE
}
} else {
if (currentOrder == SortOrder.BY_DEADLINE_AND_PRIORITY) {
SortOrder.BY_PRIORITY
} else {
SortOrder.NONE
}
}
preferences.toBuilder().setSortOrder(newSortOrder).build()
}
}
现在您可以删除 context
构造函数参数和所有 SharedPreferences 的用法。
9. 更新 TasksViewModel 以使用 UserPreferencesRepository
现在 UserPreferencesRepository
将 show_completed
和 sort_order
标志都存储在 DataStore 中并公开了 Flow<UserPreferences>
,让我们更新 TasksViewModel
以使用它们。
删除 showCompletedFlow
和 sortOrderFlow
,而是创建一个名为 userPreferencesFlow
的值,该值使用 userPreferencesRepository.userPreferencesFlow
初始化。
private val userPreferencesFlow = userPreferencesRepository.userPreferencesFlow
在 tasksUiModelFlow
创建中,将 showCompletedFlow
和 sortOrderFlow
替换为 userPreferencesFlow
。相应地替换参数。
调用 filterSortTasks
时,传入 userPreferences
中的 showCompleted
和 sortOrder
。你的代码应该如下所示
private val tasksUiModelFlow = combine(
repository.tasks,
userPreferencesFlow
) { tasks: List<Task>, userPreferences: UserPreferences ->
return@combine TasksUiModel(
tasks = filterSortTasks(
tasks,
userPreferences.showCompleted,
userPreferences.sortOrder
),
showCompleted = userPreferences.showCompleted,
sortOrder = userPreferences.sortOrder
)
}
showCompletedTasks()
函数现在应该更新为调用 userPreferencesRepository.updateShowCompleted()
。由于这是一个挂起函数,所以在 viewModelScope
中创建一个新的协程
fun showCompletedTasks(show: Boolean) {
viewModelScope.launch {
userPreferencesRepository.updateShowCompleted(show)
}
}
userPreferencesRepository
函数 enableSortByDeadline()
和 enableSortByPriority()
现在是挂起函数,因此也应该在 viewModelScope
中启动的新协程中调用它们
fun enableSortByDeadline(enable: Boolean) {
viewModelScope.launch {
userPreferencesRepository.enableSortByDeadline(enable)
}
}
fun enableSortByPriority(enable: Boolean) {
viewModelScope.launch {
userPreferencesRepository.enableSortByPriority(enable)
}
}
清理 UserPreferencesRepository
让我们删除不再需要的字段和方法。你应该可以删除以下内容
_sortOrderFlow
sortOrderFlow
updateSortOrder()
private val sortOrder: SortOrder
private val sharedPreferences
我们的应用现在应该可以成功编译了。让我们运行它,看看 show_completed
和 sort_order
标志是否正确保存。
查看代码实验室仓库的 proto_datastore
分支,以比较你的更改。
10. 总结
现在你已经迁移到 Proto DataStore,让我们回顾一下我们学到的内容
- SharedPreferences 带有一系列缺点 - 从在 UI 线程上调用看似安全的同步 API,到没有错误信号机制,再到缺少事务性 API 等等。
- DataStore 是 SharedPreferences 的替代方案,它解决了 API 的大多数缺点。
- DataStore 具有使用 Kotlin 协程和 Flow 的完全异步 API,处理数据迁移,保证数据一致性并处理数据损坏。