Preferences DataStore

1. 准备工作

在之前的 Codelab 中,您学习了如何使用 Room(一个数据库抽象层)将数据保存在 SQLite 数据库中。此 Codelab 将介绍 Jetpack DataStore。DataStore 基于 Kotlin 协程和 Flow 构建,提供以下两种不同的实现:Proto DataStore(用于存储类型化对象)和 Preferences DataStore(用于存储键值对)。

在此 Codelab 中,您将通过实操练习学习如何使用 Preferences DataStore。Proto DataStore 不在此 Codelab 的范围内。

前提条件

  • 熟悉 Android 架构组件 ViewModelLiveDataFlow,并了解如何使用 ViewModelProvider.Factory 实例化 ViewModel
  • 熟悉并发基础知识。
  • 了解如何使用协程管理长时间运行的任务。

学习内容

  • 什么是 DataStore?为什么应该使用它?何时应该使用它?
  • 如何将 Preferences DataStore 添加到应用中?

所需条件

  • Words 应用的起始代码(与之前的 Codelab 中的 Words 应用解决方案代码相同)。
  • 一台安装了 Android Studio 的计算机。

下载此 Codelab 的起始代码

在此 Codelab 中,您将基于之前的解决方案代码扩展 Words 应用的功能。起始代码可能包含您在之前的 Codelab 中已熟悉的代码。

如需从 GitHub 获取此 Codelab 的代码并在 Android Studio 中打开它,请按以下步骤操作。

  1. 启动 Android Studio。
  2. Welcome to Android Studio 窗口中,点击 Get from VCS

61c42d01719e5b6d.png

  1. Get from Version Control 对话框中,确保为 Version Control 选择 Git

9284cfbe17219bbb.png

  1. 将提供的代码网址粘贴到 URL 框中。
  2. (可选)将 Directory 从建议的默认值更改为其他目录。

5ddca7dd0d914255.png

  1. 点击 Clone。Android Studio 开始提取代码。
  2. 等待 Android Studio 打开项目。
  3. 针对 Codelab 起始代码、应用或解决方案代码选择正确的模块。

2919fe3e0c79d762.png

  1. 点击 Run 按钮 8de56cba7583251f.png 以构建并运行代码。

2. 起始应用概览

Words 应用包含两个屏幕:第一个屏幕显示可供用户选择的字母;第二个屏幕显示以所选字母开头的单词的列表。

此应用有一个菜单选项,供用户在字母的列表布局和网格布局之间切换。

  1. 下载起始代码,在 Android Studio 中将其打开并运行应用。字母将采用线性布局显示。
  2. 点按右上角的菜单选项。布局将切换为网格布局。
  3. 退出此应用并重新启动它。您可以在 Android Studio 中使用 Stop ‘app' f782441b99bdd0a4.pngRun ‘app' d203bd07cbce5954.png 选项执行此操作。请注意,重新启动此应用后,字母重新以线性布局显示,而非采用网格布局。

由此可见,系统未保留用户选择。此 Codelab 将介绍如何解决此问题。

构建内容

  • 在此 Codelab 中,您将学习如何使用 Preferences DataStore 在 DataStore 中持久保留布局设置。

3. Preferences DataStore 简介

Preferences DataStore 非常适合用于简单的小型数据集,例如用来存储详细登录信息、深色模式设置、字体大小等。DataStore 并不适用于复杂的数据集,例如网上便利店商品目录或学生数据库。如需存储大型或复杂数据集,请考虑使用 Room 而不要使用 DataStore。

使用 Jetpack DataStore 库,您可以创建一个简单、安全的异步 API 来存储数据。它提供两种不同的实现:Preferences DataStore 和 Proto DataStore。虽然 Preferences DataStore 和 Proto DataStore 都允许保存数据,但它们保存数据的方式不同:

  • Preferences DataStore 根据键访问和存储数据,而无需事先定义架构(数据库模型)。
  • Proto DataStore 使用协议缓冲区来定义架构。使用协议缓冲区(即 Protobuf),您可以持久保留强类型数据。与 XML 和其他类似的数据格式相比,协议缓冲区速度更快、占用空间更小、使用更简单,并且更清楚明了。

Room 对比 Datastore:适用情形

如果您的应用需要以 SQL 等结构化格式存储大型/复杂数据,请考虑使用 Room。不过,如果您只需要存储能以键值对形式保存的简单或少量数据,那么 DataStore 就是理想的选择。

Proto DataStore 对比 Preferences DataStore:适用情形

Proto DataStore 具有类型安全和高效的优点,但需要进行配置和设置。如果您的应用数据足够简单,能够以键值对的形式保存,那么 Preferences DataStore 就是更合适的选择,因为它的设置要容易得多。

将 Preferences DataStore 添加为依赖项

若要将 DataStore 与应用集成,首先要将其添加为依赖项。

  1. build.gradle(Module: Words.app) 中,添加以下依赖项:
implementation "androidx.datastore:datastore-preferences:1.0.0"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1"

4. 创建 Preferences DataStore

  1. 添加一个名为 data 的软件包,并在其中创建一个名为 SettingsDataStore 的 Kotlin 类。
  2. SettingsDataStore 类中添加一个类型为 Context 的构造函数参数。
class SettingsDataStore(context: Context) {}
  1. SettingsDataStore 类之外,声明一个名为 LAYOUT_PREFERENCES_NAMEprivate const val,并为其分配字符串值 layout_preferences。这是您在下一步中将要实例化的 Preferences Datastore 的名称。
private const val LAYOUT_PREFERENCES_NAME = "layout_preferences"
  1. 还是在该类之外,使用 preferencesDataStore 委托创建一个 DataStore 实例。因为您要使用 Preferences Datastore,所以需要传递 Preferences 作为数据存储区类型。此外,还要将数据存储区的 name 设为 LAYOUT_PREFERENCES_NAME

完成后的代码如下所示:

private const val LAYOUT_PREFERENCES_NAME = "layout_preferences"

// Create a DataStore instance using the preferencesDataStore delegate, with the Context as
// receiver.
private val Context.dataStore : DataStore<Preferences> by preferencesDataStore(
   name = LAYOUT_PREFERENCES_NAME
)

5. 实现 SettingsDataStore 类

如前所述,Preferences DataStore 以键值对形式存储数据。在此步骤中,您将定义存储布局设置所需的键,并定义要写入 Preferences DataStore 和从中读取的函数。

键类型函数

Preferences DataStore 不像 Room 那样使用预定义的架构,而是使用相应的键类型函数为存储在 DataStore<Preferences> 实例中的每个值定义一个键。例如,如需为 int 值定义一个键,请使用 intPreferencesKey();如需为 string 值定义一个键,请使用 stringPreferencesKey()。通常,这些函数的名称以您要根据键存储的数据类型为前缀。

data\SettingsDataStore 类中需实现以下各项:

  1. 为了实现 SettingsDataStore 类,首先要创建一个用于存储布尔值的键,该布尔值将指定用户设置是否为线性布局。创建一个名为 IS_LINEAR_LAYOUT_MANAGERprivate 类属性,并使用 booleanPreferencesKey()(传入 is_linear_layout_manager 键名称作为函数参数)对其进行初始化。
private val IS_LINEAR_LAYOUT_MANAGER = booleanPreferencesKey("is_linear_layout_manager")

写入 Preferences DataStore

现在,该使用所定义的键并将布尔值布局设置存储在 DataStore 中了。Preferences DataStore 提供了一个 edit() 挂起函数,用于以事务方式更新 DataStore 中的数据。该函数的 transform 参数接受代码块,您可以在其中根据需要更新值。transform 代码块中的所有代码会被视为单个事务。在后台,事务工作将移至 Dispacter.IO。因此,在调用 edit() 函数时不要忘记将函数设为 suspend

  1. 创建一个名为 saveLayoutToPreferencesStore()suspend 函数,其接受以下两个参数:一个是布局设置参数 Boolean,另一个是 Context
suspend fun saveLayoutToPreferencesStore(isLinearLayoutManager: Boolean, context: Context) {

}
  1. 实现上面的函数,调用 dataStore.edit() 并传入一个代码块以存储新值。
suspend fun saveLayoutToPreferencesStore(isLinearLayoutManager: Boolean, context: Context) {
   context.dataStore.edit { preferences ->
       preferences[IS_LINEAR_LAYOUT_MANAGER] = isLinearLayoutManager
   }
}

从 Preferences DataStore 读取

Preferences DataStore 会公开 Flow<Preferences> 中存储的数据,每当偏好设置发生变化时,Flow<Preferences> 就会发出该数据。您不需要公开整个 Preferences 对象,只需公开 Boolean 值。为此,我们要映射 Flow<Preferences> 并获取所需的 Boolean 值。

  1. 公开一个基于 dataStore.data: Flow<Preferences> 构造的 preferenceFlow: Flow<UserPreferences>,进行映射以检索 Boolean 偏好设置。由于首次运行时 Datastore 是空的,因此默认情况下会返回 true
val preferenceFlow: Flow<Boolean> = context.dataStore.data
   .map { preferences ->
       // On the first run of the app, we will use LinearLayoutManager by default
       preferences[IS_LINEAR_LAYOUT_MANAGER] ?: true
   }
  1. 添加以下导入内容(如果未自动导入):
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map

异常处理

当 DataStore 在文件中读取和写入数据时,可能会在访问数据时出现 IOExceptions。若要处理这些异常,可以使用 catch() 运算符捕获异常。

  1. 如果在读取数据时遇到错误,SharedPreference DataStore 会抛出 IOException。在 preferenceFlow 声明中的 map() 前面,使用 catch() 运算符捕获 IOException 并发出 emptyPreferences()。为简单起见,我们预计此处不会出现任何其他类型的异常,因此如果出现了其他类型的异常,请重新抛出该异常。
val preferenceFlow: Flow<Boolean> = context.dataStore.data
   .catch {
       if (it is IOException) {
           it.printStackTrace()
           emit(emptyPreferences())
       } else {
           throw it
       }
   }
   .map { preferences ->
       // On the first run of the app, we will use LinearLayoutManager by default
       preferences[IS_LINEAR_LAYOUT_MANAGER] ?: true
   }

现在,您的 data\SettingsDataStore 类可以使用了!

6. 使用 SettingsDataStore 类

在接下来的这个任务中,您将在 LetterListFragment 类中使用 SettingsDataStore。您要将一个观察器附加到布局设置,并相应更新界面。

LetterListFragment 中执行以下步骤:

  1. 声明一个名为 SettingsDataStore 且类型为 SettingsDataStoreprivate 类变量。将此变量设为 lateinit,因为您稍后才会对其进行初始化。
private lateinit var SettingsDataStore: SettingsDataStore
  1. onViewCreated() 函数的末尾,初始化新变量并将 requireContext() 传入 SettingsDataStore 构造函数。
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   ...
   // Initialize SettingsDataStore
   SettingsDataStore = SettingsDataStore(requireContext())
}

读取和观察数据

  1. LetterListFragment 中的 onViewCreated() 方法内,在 SettingsDataStore 初始化下方,使用 asLiveData()preferenceFlow 转换为 Livedata附加一个观察器,并传入 viewLifecycleOwner 作为所有者。
SettingsDataStore.preferenceFlow.asLiveData().observe(viewLifecycleOwner, { })
  1. 在观察器内,将新的布局设置分配给 isLinearLayoutManager 变量。调用 chooseLayout() 函数以更新 RecyclerView 布局。
SettingsDataStore.preferenceFlow.asLiveData().observe(viewLifecycleOwner, { value ->
           isLinearLayoutManager = value
           chooseLayout()
   })

完成后的 onViewCreated() 函数应如下所示:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   recyclerView = binding.recyclerView
   // Initialize SettingsDataStore
   SettingsDataStore = SettingsDataStore(requireContext())
   SettingsDataStore.preferenceFlow.asLiveData().observe(viewLifecycleOwner, { value ->
           isLinearLayoutManager = value
           chooseLayout()
   })
}

将布局设置写入 DataStore

最后一步是在用户点按菜单选项时将布局设置写入 Preferences DataStore。将数据写入 Preferences DataStore 的操作应在协程内异步执行。如需在 fragment 内执行此操作,请使用名为 LifecycleScopeCoroutineScope

LifecycleScope

fragment 等生命周期感知型组件针对应用中的逻辑作用域以及与 LiveData 的互操作层为协程提供了一流的支持。每个 Lifecycle 对象都定义了一个 LifecycleScope。在此作用域内启动的任何协程都会在 Lifecycle 所有者被销毁时取消。

  1. LetterListFragment 中的 onOptionsItemSelected() 函数内,在 R.id.action_switch_layout 末尾使用 lifecycleScope 启动协程。launch 块内,调用 saveLayoutToPreferencesStore() 并传入 isLinearLayoutManagercontext
override fun onOptionsItemSelected(item: MenuItem): Boolean {
   return when (item.itemId) {
       R.id.action_switch_layout -> {
           ...
           // Launch a coroutine and write the layout setting in the preference Datastore
           lifecycleScope.launch {
       SettingsDataStore.saveLayoutToPreferencesStore(isLinearLayoutManager, requireContext())
           }
           ...

           return true
       }
  1. 运行应用。点击菜单选项更改应用的布局。

  1. 现在,测试 Preferences DataStore 的数据持久性。将应用布局更改为网格布局。退出后重新启动应用(您可以在 Android Studio 中使用 Stop ‘app' f782441b99bdd0a4.pngRun ‘app' d203bd07cbce5954.png 选项执行此操作)。

cd2c31f27dfb5157.png

应用重新启动后,字母现在以网格布局显示而非采用线性布局。您的应用成功地保存了用户选择的布局设置!

请注意,尽管字母现在以网格布局显示,但菜单图标并未正确更新。接下来,我们看看如何解决此问题。

7. 修复菜单图标 bug

出现菜单图标 bug 的原因在于,在 onViewCreated() 中,RecyclerView 布局是根据布局设置更新的而不是根据菜单图标更新。如果在更新 RecyclerView 布局的同时重新绘制菜单,即可解决此问题。

重新绘制选项菜单

菜单一旦创建后,就不会多此一举地每一帧都重新绘制菜单。不过,您可以使用 invalidateOptionsMenu() 函数指示 Android 重新绘制选项菜单。

当您在选项菜单中进行更改(例如添加菜单项、删除菜单项或更改菜单文本或图标)时,可以调用此函数。在本例中,菜单图标已更改。调用此方法就能声明选项菜单已更改,应重新创建。下次需要显示选项菜单时,系统就会调用 onCreateOptionsMenu(android.view.Menu) 方法。

  1. LetterListFragment 中的 onViewCreated() 内,在 preferenceFlow 观察器的末尾对 chooseLayout() 的调用下方,通过对 activity 调用 invalidateOptionsMenu() 来重新绘制菜单。
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   ...
   SettingsDataStore.preferenceFlow.asLiveData().observe(viewLifecycleOwner, { value ->
           ...
           // Redraw the menu
           activity?.invalidateOptionsMenu()
   })
}
  1. 再次运行应用并更改布局。
  2. 退出后重新启动应用。请注意,菜单图标现在正确更新了。

1c8cf63c8d175aad.png

恭喜!您已成功将 Preferences DataStore 添加到您的应用中,用于保存用户选择。

8. 解决方案代码

此 Codelab 的解决方案代码位于下方所示的项目和模块中。

9. 总结

  • DataStore 有一个使用 Kotlin 协程和 Flow 的完全异步的 API,可以保证数据一致性。
  • Jetpack DataStore 是一种数据存储解决方案,让您可以使用协议缓冲区存储键值对或类型化对象。
  • DataStore 提供两种不同的实现:Preferences DataStore 和 Proto DataStore。
  • Preferences DataStore 不使用预定义的架构。
  • Preferences DataStore 使用相应的键类型函数为需要存储在 DataStore<Preferences> 实例中的每个值定义一个键。例如,如需为 int 值定义一个键,请使用 intPreferencesKey()
  • Preferences DataStore 提供了一个 edit() 函数,用于以事务方式更新 DataStore 中的数据。

10. 了解详情

博客

优先使用 Jetpack DataStore 存储数据