1. 准备工作
在之前的 Codelab 中,您学习了如何使用 Room(一个数据库抽象层)将数据保存在 SQLite 数据库中。此 Codelab 将介绍 Jetpack DataStore。DataStore 基于 Kotlin 协程和 Flow 构建,提供以下两种不同的实现:Proto DataStore(用于存储类型化对象)和 Preferences DataStore(用于存储键值对)。
在此 Codelab 中,您将通过实操练习学习如何使用 Preferences DataStore。Proto DataStore 不在此 Codelab 的范围内。
前提条件
- 熟悉 Android 架构组件
ViewModel
、LiveData
和Flow
,并了解如何使用ViewModelProvider.Factory
实例化ViewModel
。 - 熟悉并发基础知识。
- 了解如何使用协程管理长时间运行的任务。
学习内容
- 什么是 DataStore?为什么应该使用它?何时应该使用它?
- 如何将 Preferences DataStore 添加到应用中?
所需条件
- Words 应用的起始代码(与之前的 Codelab 中的 Words 应用解决方案代码相同)。
- 一台安装了 Android Studio 的计算机。
下载此 Codelab 的起始代码
在此 Codelab 中,您将基于之前的解决方案代码扩展 Words 应用的功能。起始代码可能包含您在之前的 Codelab 中已熟悉的代码。
如需从 GitHub 获取此 Codelab 的代码并在 Android Studio 中打开它,请按以下步骤操作。
- 启动 Android Studio。
- 在 Welcome to Android Studio 窗口中,点击 Get from VCS。
- 在 Get from Version Control 对话框中,确保为 Version Control 选择 Git。
- 将提供的代码网址粘贴到 URL 框中。
- (可选)将 Directory 从建议的默认值更改为其他目录。
- 点击 Clone。Android Studio 开始提取代码。
- 等待 Android Studio 打开项目。
- 针对 Codelab 起始代码、应用或解决方案代码选择正确的模块。
- 点击 Run 按钮 以构建并运行代码。
2. 起始应用概览
Words 应用包含两个屏幕:第一个屏幕显示可供用户选择的字母;第二个屏幕显示以所选字母开头的单词的列表。
此应用有一个菜单选项,供用户在字母的列表布局和网格布局之间切换。
- 下载起始代码,在 Android Studio 中将其打开并运行应用。字母将采用线性布局显示。
- 点按右上角的菜单选项。布局将切换为网格布局。
- 退出此应用并重新启动它。您可以在 Android Studio 中使用 Stop ‘app' 和 Run ‘app' 选项执行此操作。请注意,重新启动此应用后,字母重新以线性布局显示,而非采用网格布局。
由此可见,系统未保留用户选择。此 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 与应用集成,首先要将其添加为依赖项。
- 在
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
- 添加一个名为
data
的软件包,并在其中创建一个名为SettingsDataStore
的 Kotlin 类。 - 在
SettingsDataStore
类中添加一个类型为Context
的构造函数参数。
class SettingsDataStore(context: Context) {}
- 在
SettingsDataStore
类之外,声明一个名为LAYOUT_PREFERENCES_NAME
的private const val
,并为其分配字符串值layout_preferences
。这是您在下一步中将要实例化的 Preferences Datastore 的名称。
private const val LAYOUT_PREFERENCES_NAME = "layout_preferences"
- 还是在该类之外,使用
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
类中需实现以下各项:
- 为了实现
SettingsDataStore
类,首先要创建一个用于存储布尔值的键,该布尔值将指定用户设置是否为线性布局。创建一个名为IS_LINEAR_LAYOUT_MANAGER
的private
类属性,并使用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
。
- 创建一个名为
saveLayoutToPreferencesStore()
的suspend
函数,其接受以下两个参数:一个是布局设置参数 Boolean,另一个是Context
。
suspend fun saveLayoutToPreferencesStore(isLinearLayoutManager: Boolean, context: Context) {
}
- 实现上面的函数,调用
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
值。
- 公开一个基于
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
}
- 添加以下导入内容(如果未自动导入):
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
异常处理
当 DataStore 在文件中读取和写入数据时,可能会在访问数据时出现 IOExceptions
。若要处理这些异常,可以使用 catch()
运算符捕获异常。
- 如果在读取数据时遇到错误,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
中执行以下步骤:
- 声明一个名为
SettingsDataStore
且类型为SettingsDataStore
的private
类变量。将此变量设为lateinit
,因为您稍后才会对其进行初始化。
private lateinit var SettingsDataStore: SettingsDataStore
- 在
onViewCreated()
函数的末尾,初始化新变量并将requireContext()
传入SettingsDataStore
构造函数。
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
...
// Initialize SettingsDataStore
SettingsDataStore = SettingsDataStore(requireContext())
}
读取和观察数据
- 在
LetterListFragment
中的onViewCreated()
方法内,在SettingsDataStore
初始化下方,使用asLiveData
()
将preferenceFlow
转换为Livedata
。附加一个观察器,并传入viewLifecycleOwner
作为所有者。
SettingsDataStore.preferenceFlow.asLiveData().observe(viewLifecycleOwner, { })
- 在观察器内,将新的布局设置分配给
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 内执行此操作,请使用名为 LifecycleScope
的 CoroutineScope
。
LifecycleScope
fragment 等生命周期感知型组件针对应用中的逻辑作用域以及与 LiveData
的互操作层为协程提供了一流的支持。每个 Lifecycle
对象都定义了一个 LifecycleScope
。在此作用域内启动的任何协程都会在 Lifecycle
所有者被销毁时取消。
- 在
LetterListFragment
中的onOptionsItemSelected()
函数内,在R.id.
action_switch_layout
末尾使用lifecycleScope
启动协程。在launch
块内,调用saveLayoutToPreferencesStore()
并传入isLinearLayoutManager
和context
。
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
}
- 运行应用。点击菜单选项更改应用的布局。
- 现在,测试 Preferences DataStore 的数据持久性。将应用布局更改为网格布局。退出后重新启动应用(您可以在 Android Studio 中使用 Stop ‘app' 和 Run ‘app' 选项执行此操作)。
应用重新启动后,字母现在以网格布局显示而非采用线性布局。您的应用成功地保存了用户选择的布局设置!
请注意,尽管字母现在以网格布局显示,但菜单图标并未正确更新。接下来,我们看看如何解决此问题。
7. 修复菜单图标 bug
出现菜单图标 bug 的原因在于,在 onViewCreated()
中,RecyclerView 布局是根据布局设置更新的而不是根据菜单图标更新。如果在更新 RecyclerView 布局的同时重新绘制菜单,即可解决此问题。
重新绘制选项菜单
菜单一旦创建后,就不会多此一举地每一帧都重新绘制菜单。不过,您可以使用 invalidateOptionsMenu()
函数指示 Android 重新绘制选项菜单。
当您在选项菜单中进行更改(例如添加菜单项、删除菜单项或更改菜单文本或图标)时,可以调用此函数。在本例中,菜单图标已更改。调用此方法就能声明选项菜单已更改,应重新创建。下次需要显示选项菜单时,系统就会调用 onCreateOptionsMenu(android.view.Menu)
方法。
- 在
LetterListFragment
中的onViewCreated()
内,在preferenceFlow
观察器的末尾对chooseLayout()
的调用下方,通过对activity
调用invalidateOptionsMenu()
来重新绘制菜单。
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
...
SettingsDataStore.preferenceFlow.asLiveData().observe(viewLifecycleOwner, { value ->
...
// Redraw the menu
activity?.invalidateOptionsMenu()
})
}
- 再次运行应用并更改布局。
- 退出后重新启动应用。请注意,菜单图标现在正确更新了。
恭喜!您已成功将 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
中的数据。