使用 Preferences DataStore

1. 简介

什么是 DataStore?

DataStore 是一个经过改进的新型数据存储解决方案,旨在替代 SharedPreferences。该方案依托于 Kotlin 协程和 Flow 构建而成,可提供两种不同的实现方式:用于存储输入对象的 Proto DataStore(由 协议缓冲区支持);以及用于存储键值对的 Preferences DataStore。数据可支持采用异步、一致和事务的方式进行存储,从而解决了 SharedPreferences 的一些缺陷。

您将学习的内容

  • DataStore 是什么以及为何要使用该方案。
  • 如何将 DataStore 添加到项目。
  • Preferences 与 Proto DataStore 之间的差异以及各自的优势。
  • 如何使用 Preferences DataStore。
  • 如何从 SharedPreferences 迁移到 Preferences DataStore。

您将构建的内容

在本 Codelab 中,您将从一个示例应用开始,显示可通过完成状态过滤以及可按优先级和截止日期排序的任务列表。

ddda77e1b466b18b.gif

系统会将由"*Show completed tasks"(显示已完成的任务)*过滤的布尔标志保存在内存中。使用 SharedPreferences 对象,将排序顺序保存到磁盘。

在本 Codelab 中,您将完成以下任务,学习如何使用 Preferences DataStore:

  • 将已完成的状态过滤保存在 Datastore 中。
  • 将排序顺序从 SharedPreferences 迁移到 DataStore。

我们建议您也完成 Proto DataStore Codelab,如此可更好地了解两者之间的差异。

您需要具备的条件

2. 准备工作

在此步骤中,您需下载整个 Codelab 的代码,然后运行一个简单的示例应用。

为助您尽快上手,我们为您准备了一个起始项目作为构建基础。

如果您已安装 git,则只需运行下面的命令即可。要检查是否已安装 git,请在终端或命令行中输入 git --version,并验证其可正确执行。

 git clone https://github.com/googlecodelabs/android-datastore

初始状态位于主分支中。解决方案代码位于 preferences_datastore 分支中。

如果您没有 git,可点击以下按钮下载本 Codelab 的所有代码:

下载源代码

  1. 解压缩代码,然后在 Android Studio 3.6 或更高版本中打开项目。
  2. 在设备或模拟器上运行应用运行配置。

991e709b8ea0ffa6.png

应用将会运行并显示任务列表:

16eb4ceb800bf131.png

3. 项目概览

应用允许您查看任务列表。每个任务都有以下属性:名称、完成状态、优先级和截止时间。

为简化需要使用的代码,应用仅允许您执行以下两项操作:

  • 切换显示已完成任务的可见性 - 默认情况下,此类任务会处于隐藏状态
  • 按优先级、按截止时间或按截止时间和优先级对任务进行排序

应用应采用" 应用架构指南"中推荐的架构。以下是每个软件包中的内容:

data

  • Task 模型类。
  • TasksRepository 类 - 负责提供任务。为简单起见,该类会返回硬编码数据并通过 Flow 公开,以表示更真实的场景。
  • UserPreferencesRepository 类 - 包含 SortOrder,定义为 enum。根据枚举值名称,将当前排序顺序在 SharedPreferences 中保存String。该类会公开同步方法以保存和获取排序顺序。

ui

  • 与通过 RecyclerView 显示 Activity 相关的类。
  • TasksViewModel 类负责界面逻辑。

TasksViewModel - 包含构建需在界面中显示的数据所需的所有元素:任务列表、显示已完成和排序顺序标志,封装在 TasksUiModel 对象中。每当其中一个值发生变化时,我们都要重新构造一个新的 TasksUiModel。为此,我们结合了 3 个元素:

  • 检索自 TasksRepositoryFlow<List<Task>>
  • 包含最新的显示已完成标志的 MutableStateFlow<Boolean>,系统仅会将该标志保存在内存中。
  • 包含最新 SortOrder 值的 MutableStateFlow<SortOrder>

为确保正确更新界面,只有在操作组件启动时,我们才会公开 LiveData<TasksUiModel>

我们的代码有几个问题:

  • 初始化 UserPreferencesRepository.sortOrder 时,我们会屏蔽磁盘 IO 上的界面线程。这可能会导致界面卡顿。
  • 系统仅会将显示已完成标志保存在内存中,这意味着每当用户打开应用时,系统都将重置该标志。像 SortOrder 一样,系统也会在应用关闭时将其留存下来。
  • 我们目前使用 SharedPreferences 存留数据,但我们会将 MutableStateFlow 保存在内存中(需要手动修改),以便在发生变化时能够收到通知。如果在应用的其他位置修改 了值,这很容易中断。
  • UserPreferencesRepository 中,我们公开了两个更新排序顺序的方法:enableSortByDeadline()enableSortByPriority()。这两个方法都依赖于当前的排序顺序值,但是,如果在一个方法完成之前调用了另一个方法,我们最终将得到错误的最终值。更重要的是,在界面线程上调用这些方法时可能会导致界面卡顿并违反严 格模式。

尽管显示已完成和排序顺序标志都是用户首选项,但目前系统会将其表示为两个不同的对象。 因此,我们的目标之一是在 UserPreferences 类下统一这两个标志。

下面来了解如何使用 DataStore 帮助我们解决这些问题。

4. Datastore - 基础知识

您可能经常发现自己需要存储小型或简单的数据集。您可能在过去为此使用过 SharedPreferences,但是这个 API 也有一系列缺陷。Jetpack DataStore 库旨在解决这些问题,创建一个简单、安全和异步的 API 来存储数据。您可通过 2 种不同的实现方法来应用该库:

  • Preferences DataStore
  • Proto DataStore

功能

SharedPreferences

PreferencesDataStore

ProtoDataStore

异步 API

✅(仅用于通过监听器读取更改的值)

✅(通过 Flow

✅(通过 Flow

同步 API

✅(但调用界面线程并不安全)

可安全调用界面线程

❌*

✅(系统会将工作转移到 Dispatchers.IO
后台)

✅(系统会将工作转移到 Dispatchers.IO
后台)

可以报告错误

避免运行时异常

❌**

具有高度一致性保证的事务性 API

处理数据迁移

✅(通过 SharedPreferences)

✅(通过 SharedPreferences)

类型安全

✅ 使用协议缓冲区

  • SharedPreferences 有一个同步 API,看似可以安全调用界面线程,但实际上确在执行磁盘 I/O 操作。此外,apply() 会屏蔽 fsync() 上的界面线程。每当任何服务启动或停止,以及每当操作组件在应用的任何位置启动或停止时,都会触发挂起的 fsync() 调用。界面线程在 apply() 计划的挂起 fsync() 调用上受到了屏蔽,其通常会成为 ANR 的来源。

**SharedPreferences 在运行时异常时会抛出解析错误。

Preferences 与 Proto DataStore 比较

虽然 Preferences 和 Proto DataStore 都可保存数据,但其操作方式不同:

  • 与 SharedPreferences 一样,PreferenceDatastore 会基于键值来访问数据,无需预先定义模式。
  • Proto DataStore 使用 协议缓冲区定义模式。使用协议缓冲区支持存留强类型数据。 与 XML 及其他类似的数据格式相比,这种数据更快、更小、更简单且更明确。虽然 Proto DataStore 需要您学习一种新的序列化机制,但我们认为,Proto DataStore 带来的强类型优势值得您为之付出。

Room 与 Datastore 比较

如果您需要部分更新、引用完整性或大型/复杂数据集,则应考虑使用 Room 而不是 DataStore。DataStore 是小型或简单数据集的理想选择,不支持部分更新或引用完整性。

5. Preferences DataStore 概述

Preferences DataStore API 与 SharedPreferences 相似,但有几个显著区别:

  • 以事务方式处理数据更新
  • 公开表示当前数据状态的流程
  • 没有数据存留方法(apply()commit()
  • 不返回对其内部状态的可变引用
  • 使用输入的键值公开类似于 MapMutableMap 的 API

我们来看看如何将其添加到项目中以及如何将 SharedPreferences 迁移到 DataStore。

添加依赖项

更新 build.gradle 文件以添加以下 Preference DataStore 依赖项:

implementation "androidx.datastore:datastore-preferences:1.0.0-alpha01"

6. 在 Preferences DataStore 中存留数据

尽管显示已完成和排序顺序标志都是用户首选项,但目前系统会将其表示为两个不同的对象。因此,我们的目标之一是在 UserPreferences 类下统一这两个标志,并使用 DataStore 将其存储在 UserPreferencesRepository 中。现在,系统已将显示已完成标志保存在内存 TasksViewModel 中。

首先,在 UserPreferencesRepository 中创建一个 UserPreferences 数据类。现在,该类应该只有一个字段:showCompleted。稍后我们将添加排序顺序。

data class UserPreferences(val showCompleted: Boolean)

创建 DataStore

让我们使用 context.createDataStoreFactory() 方法在 UserPreferencesRepository 中创建 DataStore<Preferences> 私有字段。Preferences DataStore 的名称是必填参数。

private val dataStore: DataStore<Preferences> =
        context.createDataStore(name = "user")

从 Preferences DataStore 中读取数据

Preferences DataStore 会公开存储在 Flow<Preferences> 中的数据,每次更改首选项时都会发出该数据。我们不想公开整个 Preferences 对象,而是公开 UserPreferences 对象。为此,我们必须映射 Flow<Preferences>,根据键值获取我们感兴趣的布尔值,然后构造一个 UserPreferences 对象。

因此,我们需要做的第一件事就是定义 show completed 键值 - 这是一个 Preferences.Key<Boolean> 值,我们可以将其声明为私有 PreferencesKeys 对象的成员。

private object PreferencesKeys {
  val SHOW_COMPLETED = preferencesKey<Boolean>("show_completed")
}

让我们公开一个基于 dataStore.data: Flow<Preferences> 构造的 userPreferencesFlow: Flow<UserPreferences>,然后对其进行映射,以检索正确的首选项:

val userPreferencesFlow: Flow<UserPreferences> = dataStore.data
    .map { preferences ->
        // Get our show completed value, defaulting to false if not set:
        val showCompleted = preferences[PreferencesKeys.SHOW_COMPLETED]?: false
        UserPreferences(showCompleted)
    }

处理读取数据时的异常

由于 DataStore 会从文件中读取数据,所以在读取数据出错时,系统将抛出 IOExceptions。我们可以在 map() 之前使用 catch() 流程运算符解决这些问题,如果抛出的异常为 IOException,则发出 emptyPreferences()。如果抛出了不同类型的异常,最好重新抛出。

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) {
            emit(emptyPreferences())
        } else {
            throw exception
        }
    }.map { preferences ->
        // Get our show completed value, defaulting to false if not set:
        val showCompleted = preferences[PreferencesKeys.SHOW_COMPLETED]?: false
        UserPreferences(showCompleted)
    }

将数据写入 Preferences Datastore

为了写入数据,DataStore 提供了一个暂挂的 DataStore.edit(transform: suspend (MutablePreferences) -> Unit) 函数,该函数可接受 transform 块,助我们以事务方式更新 DataStore 中的状态。

传递到转换块的 MutablePreferences 将是最新的,其中包含之前运行的任何修改。系统将在 transform 完成后和 edit 完成前,将 transform 块中对 MutablePreferences 的所有更改应用于磁盘。在 MutablePreferences 中设置一个值将保持所有其他首选项不变。

注意:请勿尝试在转换块外部修改 MutablePreferences

我们来创建一个暂挂函数,使我们可以更新 UserPreferencesshowCompleted 属性(称为updateShowCompleted()),以调用 dataStore.edit() 并设置新值:

suspend fun updateShowCompleted(showCompleted: Boolean) {
    dataStore.edit { preferences ->
        preferences[PreferencesKeys.SHOW_COMPLETED] = showCompleted
    }
}

如果在读取或写入磁盘时遇到错误,edit() 可抛出 IOException。如果转换块中发生任何其他错误,则由 edit() 抛出。

此时,应用应该会进行编译,但并不会使用我们刚才在 UserPreferencesRepository 中创建的功能。

7. SharedPreferences 与 Preferences DataStore

系统会将排序顺序保存在 SharedPreferences 中。让我们将其移至 DataStore。为此,首先更新 UserPreferences 来存储排序顺序:

data class UserPreferences(
    val showCompleted: Boolean,
    val sortOrder: SortOrder
)

从 SharedPreferences 迁移

为了能够将其迁移到 DataStore,我们需要更新 DataStore 构建器以将 SharedPreferencesMigration 传递到迁移列表中。DataStore 将能够自动为我们从 SharedPreferences 迁移到 DataStore。必须先运行迁移,才能在 DataStore 中访问任意数据。这意味着只有在迁移成功之后,DataStore.data 才能发出值,DataStore.edit() 才能更新数据。

注意:系统仅会将键值从 SharedPreferences 中迁移一次,因此一旦将代码迁移到 DataStore,就应该停止使用旧的 SharedPreferences。

private val dataStore: DataStore<Preferences> =
    context.createDataStore(
        name = USER_PREFERENCES_NAME,
        migrations = listOf(SharedPreferencesMigration(context, USER_PREFERENCES_NAME))
    )

private object PreferencesKeys {
    ...
    // Note: this has the same name that we used with SharedPreferences.
    val SORT_ORDER = preferencesKey<String>("sort_order")
}

系统会将所有键值都迁移到我们的 DataStore,并从用户首选项 SharedPreferences 中删除。现在,我们可基于 SORT_ORDER 键值,在 Preferences 中获取并更新 SortOrder

从 DataStore 读取排序顺序

让我们更新 userPreferencesFlow 以检索 map() 转换中的排序顺序:

val userPreferencesFlow: Flow<UserPreferences> = dataStore.data
    .catch { exception ->
        if (exception is IOException) {
            emit(emptyPreferences())
        } else {
            throw exception
        }
    }.map { preferences ->
        // Get the sort order from preferences and convert it to a [SortOrder] object
        val sortOrder =
            SortOrder.valueOf(
                preferences[PreferencesKeys.SORT_ORDER] ?: SortOrder.NONE.name)

        // Get our show completed value, defaulting to false if not set:
        val showCompleted = preferences[PreferencesKeys.SHOW_COMPLETED] ?: false
        UserPreferences(showCompleted, sortOrder)
    }

将排序顺序保存到 DataStore

目前,UserPreferencesRepository 只公开了设置排序顺序标志的同步方式,并且存在并发问题。我们公开了两个更新排序顺序的方法:enableSortByDeadline()enableSortByPriority();这两个方法都依赖于当前的排序顺序值,但是,如果在一个方法完成之前调用了另一个方法,我们最终将得到错误的最终值。

由于 DataStore 可保证系统以事务方式进行数据更新,因此此问题将不复存在。让我们进行以下更改:

  • enableSortByDeadline()enableSortByPriority() 更新为使用 dataStore.edit()suspend 函数。
  • edit() 的转换块中,我们将从 Preferences 参数获取 currentOrder,而不是从 _sortOrderFlow 字段中进行检索。
  • 我们可以直接更新 Preferences 中的排序顺序,不需要调用 updateSortOrder(newSortOrder)

其实现如下所示。

suspend fun enableSortByDeadline(enable: Boolean) {
    // edit handles data transactionally, ensuring that if the sort is updated at the same
    // time from another thread, we won't have conflicts
    dataStore.edit { preferences ->
        // Get the current SortOrder as an enum
        val currentOrder = SortOrder.valueOf(
            preferences[PreferencesKeys.SORT_ORDER] ?: SortOrder.NONE.name
        )

        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[PreferencesKeys.SORT_ORDER] = newSortOrder.name
    }
}

suspend fun enableSortByPriority(enable: Boolean) {
    // edit handles data transactionally, ensuring that if the sort is updated at the same
    // time from another thread, we won't have conflicts
    dataStore.edit { preferences ->
        // Get the current SortOrder as an enum
        val currentOrder = SortOrder.valueOf(
            preferences[PreferencesKeys.SORT_ORDER] ?: SortOrder.NONE.name
        )

        val newSortOrder =
            if (enable) {
                if (currentOrder == SortOrder.BY_DEADLINE) {
                    SortOrder.BY_DEADLINE_AND_PRIORITY
                } else {
                    SortOrder.BY_PRIORITY
                }
            } else {
                if (currentOrder == SortOrder.BY_DEADLINE_AND_PRIORITY) {
                    SortOrder.BY_DEADLINE
                } else {
                    SortOrder.NONE
                }
            }
        preferences[PreferencesKeys.SORT_ORDER] = newSortOrder.name
    }
}

8. 更新 TasksViewModel 以使用 UserPreferencesRepository

现在,UserPreferencesRepository 将显示已完成和排序顺序标志都存储在 DataStore 中,并公开了 Flow<UserPreferences>,让我们更新 TasksViewModel 以使用它们。

删除 showCompletedFlowsortOrderFlow,然后创建一个名为 userPreferencesFlow 的值,用 userPreferencesRepository.userPreferencesFlow 进行初始化:

private val userPreferencesFlow = userPreferencesRepository.userPreferencesFlow

tasksUiModelFlow 创建过程中,将 showCompletedFlowsortOrderFlow 替换为 userPreferencesFlow。相应地替换参数。

调用 filterSortTasks 时,传入 userPreferencesshowCompletedsortOrder。您的代码应如下所示:

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

我们的应用现在应该编译成功了。我们来运行一下,看看显示已完成和排序顺序标志是否已得到正确保存。

检视 Codelab 存储库的 Preferences 分支以比较您的更改。

9. 小结

现在,您已迁移到 Preferences DataStore,我们来回顾一下所学内容:

  • SharedPreferences 有一系列的缺陷,例如同步 API 看似可以安全调用界面线程,但没有报告错误的机制;缺少事务性 API 等。
  • 使用 DataStore 替代 SharedPreferences,可解决 API 的大多数缺陷。
  • DataStore 具有使用 Kotlin 协程和流程的完全异步 API,可处理数据迁移、保证数据一致性和解决数据损坏问题。