1. 准备工作
简介
在本单元中,您学习了如何使用 SQL 和 Room 在设备上本地保存数据。SQL 和 Room 是强大的工具。但是,在不需要存储关系数据的情况下,DataStore 可以提供一个简单的解决方案。DataStore Jetpack 组件是存储少量简单数据集的绝佳方式,开销很低。DataStore 有两种不同的实现,Preferences DataStore 和 Proto DataStore。
Preferences DataStore存储键值对。值可以是 Kotlin 的基本数据类型,例如String、Boolean和Integer。它不存储复杂数据集,也不需要预定义架构。Preferences DataStore的主要用例是在设备上存储用户偏好设置。Proto DataStore存储自定义数据类型。它需要一个预定义架构,用于将 proto 定义与对象结构进行映射。
本 Codelab 仅涵盖 Preferences DataStore,但您可以在 DataStore 文档中详细了解 Proto DataStore。
Preferences DataStore 是存储用户控制设置的绝佳方式,在本 Codelab 中,您将学习如何实现 DataStore 来完成这项任务!
前提条件
- 完成 Android Basics with Compose 课程,直到 使用 Room 读取和更新数据 Codelab。
您需要准备的工具
- 一台连接互联网的计算机和 Android Studio
- 一台设备或模拟器
- Dessert Release 应用的入门代码
您将构建什么
Dessert Release 应用显示了 Android 版本的列表。应用栏中的图标可在网格视图和列表视图之间切换布局。

在当前状态下,该应用不会保留布局选择。当您关闭应用时,您的布局选择不会保存,设置会恢复为默认选择。在本 Codelab 中,您将向 Dessert Release 应用添加 DataStore,并使用它来存储布局选择偏好设置。
2. 下载入门代码
点击以下链接下载此 Codelab 的所有代码
或者,如果您愿意,可以从 GitHub 克隆 Dessert Release 代码
$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-dessert-release.git $ cd basic-android-kotlin-compose-training-dessert-release $ git checkout starter
- 在 Android Studio 中,打开
basic-android-kotlin-compose-training-dessert-release文件夹。 - 在 Android Studio 中打开 Dessert Release 应用代码。
3. 设置依赖项
在 app/build.gradle.kts 文件的 dependencies 中添加以下内容
implementation("androidx.datastore:datastore-preferences:1.0.0")
4. 实现用户偏好设置仓库
- 在
data包中,创建一个名为UserPreferencesRepository的新类。

- 在
UserPreferencesRepository构造函数中,定义一个私有值属性来表示一个Preferences类型的DataStore对象实例。
class UserPreferencesRepository(
private val dataStore: DataStore<Preferences>
){
}
DataStore 存储键值对。要访问值,必须定义一个键。
- 在
UserPreferencesRepository类中创建一个companion object。 - 使用
booleanPreferencesKey()函数定义一个键,并将名称is_linear_layout传递给它。与 SQL 表名类似,键需要使用下划线格式。此键用于访问一个布尔值,该值指示是否应显示线性布局。
class UserPreferencesRepository(
private val dataStore: DataStore<Preferences>
){
private companion object {
val IS_LINEAR_LAYOUT = booleanPreferencesKey("is_linear_layout")
}
...
}
写入 DataStore
通过向 edit() 方法传递一个 lambda,可以在 DataStore 中创建和修改值。lambda 会传入 MutablePreferences 的实例,您可以使用它来更新 DataStore 中的值。此 lambda 中的所有更新都作为一个事务执行。换句话说,更新是原子操作 — 它一次性完成。这种类型的更新可以防止出现某些值更新但其他值未更新的情况。
- 创建一个挂起函数并命名为
saveLayoutPreference()。 - 在
saveLayoutPreference()函数中,对dataStore对象调用edit()方法。
suspend fun saveLayoutPreference(isLinearLayout: Boolean) {
dataStore.edit {
}
}
- 为了使您的代码更具可读性,请为 lambda 主体中提供的
MutablePreferences定义一个名称。使用该属性设置一个值,该值使用您定义的键和传递给saveLayoutPreference()函数的布尔值。
suspend fun saveLayoutPreference(isLinearLayout: Boolean) {
dataStore.edit { preferences ->
preferences[IS_LINEAR_LAYOUT] = isLinearLayout
}
}
从 DataStore 读取
既然您已经创建了将 isLinearLayout 写入 dataStore 的方法,请按照以下步骤读取它
- 在
UserPreferencesRepository中创建一个类型为Flow<Boolean>的属性,命名为isLinearLayout。
val isLinearLayout: Flow<Boolean> =
- 您可以使用
DataStore.data属性公开DataStore的值。将isLinearLayout设置为DataStore对象的data属性。
val isLinearLayout: Flow<Boolean> = dataStore.data
data 属性是 Preferences 对象的 Flow。Preferences 对象包含 DataStore 中的所有键值对。每次 DataStore 中的数据更新时,都会向 Flow 发出一个新的 Preferences 对象。
- 使用 map 函数将
Flow<Preferences>转换为Flow<Boolean>。
此函数接受一个 lambda,该 lambda 的参数是当前的 Preferences 对象。您可以指定之前定义的键来获取布局偏好设置。请记住,如果尚未调用 saveLayoutPreference,则该值可能不存在,因此您还必须提供一个默认值。
- 指定
true以默认为线性布局视图。
val isLinearLayout: Flow<Boolean> = dataStore.data.map { preferences ->
preferences[IS_LINEAR_LAYOUT] ?: true
}
异常处理
任何时候与设备上的文件系统交互时,都有可能发生故障。例如,文件可能不存在,或者磁盘可能已满或已卸载。由于 DataStore 从文件中读取和写入数据,访问 DataStore 时可能会发生 IOExceptions。您可以使用 catch{} 运算符来捕获异常并处理这些故障。
- 在 companion object 中,实现一个不可变的
TAG字符串属性用于日志记录。
private companion object {
val IS_LINEAR_LAYOUT = booleanPreferencesKey("is_linear_layout")
const val TAG = "UserPreferencesRepo"
}
Preferences DataStore在读取数据时遇到错误会抛出IOException。在isLinearLayout初始化块中,在map()之前,使用catch{}运算符捕获IOException。
val isLinearLayout: Flow<Boolean> = dataStore.data
.catch {}
.map { preferences ->
preferences[IS_LINEAR_LAYOUT] ?: true
}
- 在 catch 块中,如果发生
IOexception,则记录错误并发出emptyPreferences()。如果抛出不同类型的异常,则优先重新抛出该异常。通过在发生错误时发出emptyPreferences(),map 函数仍然可以映射到默认值。
val isLinearLayout: Flow<Boolean> = dataStore.data
.catch {
if(it is IOException) {
Log.e(TAG, "Error reading preferences.", it)
emit(emptyPreferences())
} else {
throw it
}
}
.map { preferences ->
preferences[IS_LINEAR_LAYOUT] ?: true
}
5. 初始化 DataStore
在此 Codelab 中,您必须手动处理依赖注入。因此,您必须手动为 UserPreferencesRepository 类提供一个 Preferences DataStore。按照以下步骤将 DataStore 注入 UserPreferencesRepository。
- 找到
dessertrelease包。 - 在此目录下,创建一个名为
DessertReleaseApplication的新类并实现Application类。这是您的 DataStore 的容器。
class DessertReleaseApplication: Application() {
}
- 在
DessertReleaseApplication.kt文件内,但在DessertReleaseApplication类外部,声明一个名为LAYOUT_PREFERENCE_NAME的private const val。 - 将字符串值
layout_preferences分配给LAYOUT_PREFERENCE_NAME变量,然后您可以将其用作下一步中实例化的Preferences Datastore的名称。
private const val LAYOUT_PREFERENCE_NAME = "layout_preferences"
- 仍在
DessertReleaseApplication类主体外部,但在DessertReleaseApplication.kt文件中,使用preferencesDataStore委托创建一个类型为DataStore<Preferences>的私有值属性,名为Context.dataStore。将LAYOUT_PREFERENCE_NAME作为preferencesDataStore委托的name参数传递。
private const val LAYOUT_PREFERENCE_NAME = "layout_preferences"
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
name = LAYOUT_PREFERENCE_NAME
)
- 在
DessertReleaseApplication类主体内部,创建UserPreferencesRepository的lateinit var实例。
private const val LAYOUT_PREFERENCE_NAME = "layout_preferences"
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
name = LAYOUT_PREFERENCE_NAME
)
class DessertReleaseApplication: Application() {
lateinit var userPreferencesRepository: UserPreferencesRepository
}
- 重写
onCreate()方法。
private const val LAYOUT_PREFERENCE_NAME = "layout_preferences"
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
name = LAYOUT_PREFERENCE_NAME
)
class DessertReleaseApplication: Application() {
lateinit var userPreferencesRepository: UserPreferencesRepository
override fun onCreate() {
super.onCreate()
}
}
- 在
onCreate()方法内部,通过构造一个以dataStore作为参数的UserPreferencesRepository来初始化userPreferencesRepository。
private const val LAYOUT_PREFERENCE_NAME = "layout_preferences"
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
name = LAYOUT_PREFERENCE_NAME
)
class DessertReleaseApplication: Application() {
lateinit var userPreferencesRepository: UserPreferencesRepository
override fun onCreate() {
super.onCreate()
userPreferencesRepository = UserPreferencesRepository(dataStore)
}
}
- 在
AndroidManifest.xml文件的<application>标签内添加以下行。
<application
android:name=".DessertReleaseApplication"
...
</application>
这种方法将 DessertReleaseApplication 类定义为应用的入口点。此代码的目的是在启动 MainActivity 之前初始化在 DessertReleaseApplication 类中定义的依赖项。
6. 使用 UserPreferencesRepository
将仓库提供给 ViewModel
现在 UserPreferencesRepository 可通过依赖注入使用,您可以在 DessertReleaseViewModel 中使用它。
- 在
DessertReleaseViewModel中,创建一个UserPreferencesRepository属性作为构造函数参数。
class DessertReleaseViewModel(
private val userPreferencesRepository: UserPreferencesRepository
) : ViewModel() {
...
}
- 在
ViewModel的 companion object 中,在viewModelFactory initializer块中,使用以下代码获取DessertReleaseApplication的实例。
...
companion object {
val Factory: ViewModelProvider.Factory = viewModelFactory {
initializer {
val application = (this[APPLICATION_KEY] as DessertReleaseApplication)
...
}
}
}
}
- 创建
DessertReleaseViewModel的实例并传递userPreferencesRepository。
...
companion object {
val Factory: ViewModelProvider.Factory = viewModelFactory {
initializer {
val application = (this[APPLICATION_KEY] as DessertReleaseApplication)
DessertReleaseViewModel(application.userPreferencesRepository)
}
}
}
}
现在 ViewModel 可以访问 UserPreferencesRepository。下一步是使用您之前实现的 UserPreferencesRepository 的读写功能。
存储布局偏好设置
- 编辑
DessertReleaseViewModel中的selectLayout()函数,以访问偏好设置仓库并更新布局偏好设置。 - 回想一下,写入
DataStore是通过suspend函数异步完成的。启动一个新的协程来调用偏好设置仓库的saveLayoutPreference()函数。
fun selectLayout(isLinearLayout: Boolean) {
viewModelScope.launch {
userPreferencesRepository.saveLayoutPreference(isLinearLayout)
}
}
读取布局偏好设置
在本节中,您将重构 ViewModel 中现有的 uiState: StateFlow,以反映仓库中的 isLinearLayout: Flow。
- 删除将
uiState属性初始化为MutableStateFlow(DessertReleaseUiState)的代码。
val uiState: StateFlow<DessertReleaseUiState> =
仓库中的线性布局偏好设置有两种可能的值:true 或 false,形式为 Flow<Boolean>。此值必须映射到 UI 状态。
- 将
StateFlow设置为在isLinearLayout Flow上调用的map()集合转换的结果。
val uiState: StateFlow<DessertReleaseUiState> =
userPreferencesRepository.isLinearLayout.map { isLinearLayout ->
}
- 返回
DessertReleaseUiState数据类的实例,并传递isLinearLayout Boolean。屏幕使用此 UI 状态来确定要显示的正确字符串和图标。
val uiState: StateFlow<DessertReleaseUiState> =
userPreferencesRepository.isLinearLayout.map { isLinearLayout ->
DessertReleaseUiState(isLinearLayout)
}
UserPreferencesRepository.isLinearLayout 是一个冷 Flow,cold。但是,为了向 UI 提供状态,最好使用热流,例如 StateFlow,以便状态始终立即提供给 UI。
- 使用
stateIn()函数将Flow转换为StateFlow。 stateIn()函数接受三个参数:scope、started和initialValue。分别将viewModelScope、SharingStarted.WhileSubscribed(5_000)和DessertReleaseUiState()作为这些参数传入。
val uiState: StateFlow<DessertReleaseUiState> =
userPreferencesRepository.isLinearLayout.map { isLinearLayout ->
DessertReleaseUiState(isLinearLayout)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = DessertReleaseUiState()
)
- 启动应用。注意,您可以点击切换图标以在网格布局和线性布局之间切换。

恭喜!您已成功向您的应用添加 Preferences DataStore 以保存用户的布局偏好设置。
7. 获取解决方案代码
要下载已完成的 Codelab 的代码,可以使用这些 git 命令
$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-dessert-release.git $ cd basic-android-kotlin-compose-training-dessert-release $ git checkout main
或者,您可以将仓库下载为 zip 文件,解压并在 Android Studio 中打开。
如果您想查看解决方案代码,请在 GitHub 上查看。