1. 开始之前
简介
在本单元中,您学习了如何使用 SQL 和 Room 在设备上本地保存数据。SQL 和 Room 是强大的工具。但是,在您不需要存储关系数据的用例中,DataStore 可以提供一个简单的解决方案。DataStore Jetpack 组件是存储少量简单数据集的绝佳方式,其开销很低。DataStore 有两种不同的实现方式:Preferences DataStore
和 Proto DataStore
。
Preferences DataStore
存储键值对。这些值可以是 Kotlin 的基本数据类型,例如String
、Boolean
和Integer
。它不存储复杂的数据集。它不需要预定义模式。Preferences Datastore
的主要用例是在设备上存储用户偏好设置。Proto DataStore
存储自定义数据类型。它需要一个预定义模式,该模式将 proto 定义映射到对象结构。
本代码实验室只介绍了 Preferences DataStore
,但您可以在 DataStore 文档中阅读有关 Proto DataStore
的更多信息。
Preferences DataStore
是存储用户控制设置的绝佳方式,在本代码实验室中,您将学习如何实现 DataStore
来实现这一功能!
先决条件
- 完成 Android 基础知识与 Compose 课程中的 使用 Room 读取和更新数据 代码实验室。
您需要的东西
- 一台连接互联网的计算机以及 Android Studio
- 一台设备或模拟器
- Dessert Release 应用的入门代码
您将构建的内容
Dessert Release 应用显示了 Android 版本的列表。应用栏中的图标可在网格视图和列表视图之间切换布局。
在当前状态下,该应用不会持久化布局选择。当您关闭应用时,您的布局选择不会保存,设置将恢复为默认选择。在本代码实验室中,您将在 Dessert Release 应用中添加 DataStore
,并使用它来存储布局选择偏好设置。
2. 下载入门代码
单击以下链接下载本代码实验室的所有代码
或者,如果您愿意,也可以从 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
构造函数中,定义一个私有值属性来表示DataStore
对象实例,其类型为Preferences
。
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 中的数据更新时,都会将一个新的 Preferences
对象发射到 Flow
中。
- 使用 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 对象中,实现一个不可变的
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
在本代码实验室中,您必须手动处理依赖项注入。因此,您必须手动向 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
。将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
类主体中,创建一个lateinit var
实例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
}
- 覆盖
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 初始化器
块中,使用以下代码获取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
,它是 冷的。但是,为了向 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. 获取解决方案代码
要下载完成的代码实验室的代码,可以使用以下 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 上查看它。