使用 DataStore 在本地保存偏好设置

1. 准备工作

简介

在本单元中,您已经学习了如何使用 SQL 和 Room 在本地设备上保存数据。SQL 和 Room 是功能强大的工具。但是,在您不需要存储关系型数据的情况下,DataStore 可以提供一种简单的解决方案。Jetpack 中的 DataStore 组件非常适合存储简单的小型数据集,且开销较低。DataStore 有两种不同的实现:Preferences DataStoreProto DataStore

  • Preferences DataStore 存储键值对。这些值可以是 Kotlin 的基本数据类型,例如 StringBooleanInteger。它不存储复杂的数据集,也不需要预定义的架构。Preferences Datastore 的主要应用场景是在用户的设备上存储其偏好设置。
  • Proto DataStore 存储自定义数据的类型。它需要一个预定义的架构,用于将 proto 定义映射到对象结构。

此 Codelab 仅介绍 Preferences DataStore,但您可以在 DataStore 文档中详细了解 Proto DataStore

Preferences DataStore 非常适合存储由用户控制的设置,而此 Codelab 就将介绍如何通过实现 DataStore 来存储此类设置!

前提条件:

所需条件

  • 一台连接到互联网并安装了 Android Studio 的电脑
  • 一台设备或模拟器
  • Dessert Release 应用的起始代码

构建内容

Dessert Release 应用会显示 Android 版本列表。通过应用栏中的图标,可在网格视图和列表视图之间切换布局。

7f93523c9844380f.png 5b542bebfbd7a3a1.png

在当前状态下,应用不会保留布局选择。当您关闭应用后,系统不会保存您的布局选择,并且会将设置还原为默认选择。在此 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
  1. 在 Android Studio 中,打开 basic-android-kotlin-compose-training-dessert-release 文件夹。
  2. 在 Android Studio 中,打开 Dessert Release 应用代码。

3. 设置依赖项

将以下代码添加到 app/build.gradle.kts 文件的 dependencies 中:

implementation("androidx.datastore:datastore-preferences:1.0.0")

4. 实现用户偏好设置仓库

  1. data 软件包中,新建一个名为 UserPreferencesRepository 的类。

adf3ec7481163f56.png

  1. UserPreferencesRepository 构造函数中,定义一个不公开值属性,用于表示一个类型为 PreferencesDataStore 对象实例。
class UserPreferencesRepository(
    private val dataStore: DataStore<Preferences>
){
}

DataStore 存储键值对。如需访问某个值,您必须定义一个键。

  1. UserPreferencesRepository 类中创建一个 companion object
  2. 使用 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 内的所有更新作为单个事务执行。换句话说,这种更新是原子性的,所有更新都同时发生。此类更新可防止出现某些值已更新而其他值未更新的情况。

  1. 创建一个挂起函数,并将其命名为 saveLayoutPreference()
  2. saveLayoutPreference() 函数中,对 dataStore 对象调用 edit() 方法。
suspend fun saveLayoutPreference(isLinearLayout: Boolean) {
    dataStore.edit {

    }
}
  1. 为了提高代码的可读性,请为 lambda 正文中提供的 MutablePreferences 定义一个名称。使用该属性设置一个值,其中应包含您定义的键以及传递到 saveLayoutPreference() 函数的布尔值。
suspend fun saveLayoutPreference(isLinearLayout: Boolean) {
    dataStore.edit { preferences ->
        preferences[IS_LINEAR_LAYOUT] = isLinearLayout
    }
}

从 DataStore 中读取

现在,您已经创建了一种将 isLinearLayout 写入 dataStore 的方法,请按照下面的步骤进行读取:

  1. UserPreferencesRepository 中创建一个属性,其类型为 Flow<Boolean> 且名为 isLinearLayout
val isLinearLayout: Flow<Boolean> =
  1. 您可以使用 DataStore.data 属性公开 DataStore 值。将 isLinearLayout 设置为 DataStore 对象的 data 属性。
val isLinearLayout: Flow<Boolean> = dataStore.data

data 属性是 Preferences 对象的 FlowPreferences 对象包含 DataStore 中的所有键值对。DataStore 中的数据每次更新时,系统都会向 Flow 发出一个新的 Preferences 对象。

  1. 使用映射函数将 Flow<Preferences> 转换为 Flow<Boolean>

此函数接受将当前 Preferences 对象的 lambda 作为参数。您可以指定之前定义的键用于获取布局偏好设置。请注意,如果尚未调用 saveLayoutPreference,该值可能不存在,因此您还必须提供一个默认值。

  1. 指定该值为 true,可将默认视图设为线性布局。
val isLinearLayout: Flow<Boolean> = dataStore.data.map { preferences ->
    preferences[IS_LINEAR_LAYOUT] ?: true
}

异常处理

当您与设备上的文件系统互动时,您随时都可能遇到问题。例如,文件可能不存在,或者磁盘可能已满或已卸载。由于 DataStore 可从文件中读取和向其中写入数据,访问 DataStore 时可能会发生 IOExceptions。使用 catch{} 运算符来捕获异常并处理这些故障。

  1. 在伴生对象中,实现一个不可变的 TAG 字符串属性,用于日志记录。
private companion object {
    val IS_LINEAR_LAYOUT = booleanPreferencesKey("is_linear_layout")
    const val TAG = "UserPreferencesRepo"
}
  1. 如果系统在读取数据时遇到错误,Preferences DataStore 会抛出一个 IOException。在 isLinearLayout 初始化块中的 map() 前面,使用 catch{} 运算符捕获 IOException
val isLinearLayout: Flow<Boolean> = dataStore.data
    .catch {}
    .map { preferences ->
        preferences[IS_LINEAR_LAYOUT] ?: true
    }
  1. 在 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

  1. 找到 dessertrelease 软件包。
  2. 在此目录中,创建一个名为 DessertReleaseApplication 的新类并实现 Application 类。这是您的 DataStore 的容器。
class DessertReleaseApplication: Application() {
}
  1. DessertReleaseApplication.kt 文件内(但在 DessertReleaseApplication 类之外),声明一个名为 LAYOUT_PREFERENCE_NAMEprivate const val
  2. LAYOUT_PREFERENCE_NAME 变量分配字符串值 layout_preferences,您随后可将其用作下一步中要实例化的 Preferences Datastore 的名称。
private const val LAYOUT_PREFERENCE_NAME = "layout_preferences"
  1. 仍然在 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
)
  1. 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
}
  1. 替换 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()
    }
}
  1. 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)
    }
}
  1. AndroidManifest.xml 文件的 <application> 标记内,添加以下代码行。
<application
    android:name=".DessertReleaseApplication"
    ...
</application>

此方法将 DessertReleaseApplication 类定义为应用的入口点。此代码用于在启动 MainActivity 之前,初始化 DessertReleaseApplication 类中定义的依赖项。

6. 使用 UserPreferencesRepository

向 ViewModel 提供仓库

现在,通过依赖项注入可使用 UserPreferencesRepository,接下来您可以在 DessertReleaseViewModel 中使用它。

  1. DessertReleaseViewModel 中,创建一个 UserPreferencesRepository 属性作为构造函数参数。
class DessertReleaseViewModel(
    private val userPreferencesRepository: UserPreferencesRepository
) : ViewModel() {
    ...
}
  1. ViewModel 的伴生对象内,在 viewModelFactory initializer 块中,使用以下代码获取 DessertReleaseApplication 的一个实例。
    ...
    companion object {
        val Factory: ViewModelProvider.Factory = viewModelFactory {
            initializer {
                val application = (this[APPLICATION_KEY] as DessertReleaseApplication)
                ...
            }
        }
    }
}
  1. 创建 DessertReleaseViewModel 的一个实例,并传递 userPreferencesRepository
    ...
    companion object {
        val Factory: ViewModelProvider.Factory = viewModelFactory {
            initializer {
                val application = (this[APPLICATION_KEY] as DessertReleaseApplication)
                DessertReleaseViewModel(application.userPreferencesRepository)
            }
        }
    }
}

现在即可通过 ViewModel 访问 UserPreferencesRepository。后续步骤是,使用之前实现的 UserPreferencesRepository 的读写功能。

存储布局偏好设置

  1. 修改 DessertReleaseViewModel 中的 selectLayout() 函数,以访问偏好设置仓库并更新布局偏好设置。
  2. 回想一下,写入 DataStore 是通过 suspend 函数异步完成的。启动新协程,以便调用偏好设置仓库的 saveLayoutPreference() 函数。
fun selectLayout(isLinearLayout: Boolean) {
    viewModelScope.launch {
        userPreferencesRepository.saveLayoutPreference(isLinearLayout)
    }
}

读取布局偏好设置

在本部分中,您将重构 ViewModel 中的现有 uiState: StateFlow,以体现仓库中的 isLinearLayout: Flow

  1. 删除将 uiState 属性初始化为 MutableStateFlow(DessertReleaseUiState) 的代码。
val uiState: StateFlow<DessertReleaseUiState> =

仓库中的线性布局偏好设置有两个可能的值:true 或 false,形式为 Flow<Boolean>。此值必须映射到界面状态。

  1. StateFlow 设置为对 isLinearLayout Flow 调用的 map() 集合转换的结果。
val uiState: StateFlow<DessertReleaseUiState> =
    userPreferencesRepository.isLinearLayout.map { isLinearLayout ->
}
  1. 返回 DessertReleaseUiState 数据类的一个实例,传递 isLinearLayout Boolean。屏幕使用此界面状态来确定要显示的正确字符串和图标。
val uiState: StateFlow<DessertReleaseUiState> =
    userPreferencesRepository.isLinearLayout.map { isLinearLayout ->
        DessertReleaseUiState(isLinearLayout)
    }

UserPreferencesRepository.isLinearLayout 是一个 Flow不过,为界面提供状态时,最好使用热数据流(如 StateFlow),以便界面始终可立即获取状态。

  1. 使用 stateIn() 函数将 Flow 转换为 StateFlow
  2. stateIn() 函数接受三个参数:scopestartedinitialValue。分别为这些参数传入 viewModelScopeSharingStarted.WhileSubscribed(5_000)DessertReleaseUiState()
val uiState: StateFlow<DessertReleaseUiState> =
    userPreferencesRepository.isLinearLayout.map { isLinearLayout ->
        DessertReleaseUiState(isLinearLayout)
    }
.stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5_000),
        initialValue = DessertReleaseUiState()
    )
  1. 启动应用。请注意,您可以点击切换图标,在网格布局和线性布局之间切换。

7f93523c9844380f.png 5b542bebfbd7a3a1.png

恭喜!您已成功将 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 查看