1. 准备工作
简介
在本单元中,您已经学习了如何使用 SQL 和 Room 在本地设备上保存数据。SQL 和 Room 是功能强大的工具。但是,在您不需要存储关系型数据的情况下,DataStore 可以提供一种简单的解决方案。Jetpack 中的 DataStore 组件非常适合存储简单的小型数据集,且开销较低。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
来存储此类设置!
前提条件:
- 完成 Codelab 课程“使用 Room 读取和更新数据”和相应的“Android 之 Compose 开发基础”课程作业。
所需条件
- 一台连接到互联网并安装了 Android Studio 的电脑
- 一台设备或模拟器
- Dessert Release 应用的起始代码
构建内容
Dessert Release 应用会显示 Android 版本列表。通过应用栏中的图标,可在网格视图和列表视图之间切换布局。
在当前状态下,应用不会保留布局选择。当您关闭应用后,系统不会保存您的布局选择,并且会将设置还原为默认选择。在此 Codelab 中,您会将 DataStore
添加到 Dessert Release 应用中,并使用它来存储关于布局选择的偏好设置。
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
您可以通过将 lambda 传递给 edit()
方法来在 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
对象。
- 使用映射函数将
Flow<Preferences>
转换为Flow<Boolean>
。
此函数接受将当前 Preferences
对象的 lambda 作为参数。您可以指定之前定义的键用于获取布局偏好设置。请注意,如果尚未调用 saveLayoutPreference
,该值可能不存在,因此您还必须提供一个默认值。
- 指定该值为
true
,可将默认视图设为线性布局。
val isLinearLayout: Flow<Boolean> = dataStore.data.map { preferences ->
preferences[IS_LINEAR_LAYOUT] ?: true
}
异常处理
当您与设备上的文件系统互动时,您随时都可能遇到问题。例如,文件可能不存在,或者磁盘可能已满或已卸载。由于 DataStore
可从文件中读取和向其中写入数据,访问 DataStore
时可能会发生 IOExceptions
。使用 catch{}
运算符来捕获异常并处理这些故障。
- 在伴生对象中,实现一个不可变的
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()
,映射函数仍然可以映射到默认值。
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_PREFERENCE_NAME
变量分配字符串值layout_preferences
,您随后可将其用作下一步中要实例化的Preferences Datastore
的名称。
private const val LAYOUT_PREFERENCE_NAME = "layout_preferences"
- 仍然在
DessertReleaseApplication
类正文之外(但在DessertReleaseApplication.kt
文件中),使用preferencesDataStore
委托创建一个不公开值属性,其类型为DataStore<Preferences>
且名为Context.dataStore
。为preferencesDataStore
委托的name
参数传递LAYOUT_PREFERENCE_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
的伴生对象内,在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>
。此值必须映射到界面状态。
- 将
StateFlow
设置为对isLinearLayout Flow
调用的map()
集合转换的结果。
val uiState: StateFlow<DessertReleaseUiState> =
userPreferencesRepository.isLinearLayout.map { isLinearLayout ->
}
- 返回
DessertReleaseUiState
数据类的一个实例,传递isLinearLayout Boolean
。屏幕使用此界面状态来确定要显示的正确字符串和图标。
val uiState: StateFlow<DessertReleaseUiState> =
userPreferencesRepository.isLinearLayout.map { isLinearLayout ->
DessertReleaseUiState(isLinearLayout)
}
UserPreferencesRepository.isLinearLayout
是一个冷 Flow
。不过,为界面提供状态时,最好使用热数据流(如 StateFlow
),以便界面始终可立即获取状态。
- 使用
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 查看。