使用 DataStore 本地保存偏好设置

1. 开始之前

简介

在本单元中,您学习了如何使用 SQL 和 Room 在设备上本地保存数据。SQL 和 Room 是强大的工具。但是,在您不需要存储关系数据的用例中,DataStore 可以提供一个简单的解决方案。DataStore Jetpack 组件是存储少量简单数据集的绝佳方式,其开销很低。DataStore 有两种不同的实现方式:Preferences DataStoreProto DataStore

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

本代码实验室只介绍了 Preferences DataStore,但您可以在 DataStore 文档中阅读有关 Proto DataStore 的更多信息。

Preferences DataStore 是存储用户控制设置的绝佳方式,在本代码实验室中,您将学习如何实现 DataStore 来实现这一功能!

先决条件

您需要的东西

  • 一台连接互联网的计算机以及 Android Studio
  • 一台设备或模拟器
  • Dessert Release 应用的入门代码

您将构建的内容

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

b6e4bd0e50915b81.png 24a261db4cf2c6b8.png

在当前状态下,该应用不会持久化布局选择。当您关闭应用时,您的布局选择不会保存,设置将恢复为默认选择。在本代码实验室中,您将在 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
  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 的新类。

c4c2e90902898001.png

  1. UserPreferencesRepository 构造函数中,定义一个私有值属性来表示 DataStore 对象实例,其类型为 Preferences
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 中的数据更新时,都会将一个新的 Preferences 对象发射到 Flow 中。

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

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

  1. 指定 true 以默认使用线性布局视图。
val isLinearLayout: Flow<Boolean> = dataStore.data.map { preferences ->
    preferences[IS_LINEAR_LAYOUT] ?: true
}

异常处理

无论何时您与设备上的文件系统进行交互,都有可能出现故障。例如,文件可能不存在,或者磁盘可能已满或已卸载。由于 DataStore 从文件读取和写入数据,因此在访问 DataStore 时可能会发生 IOExceptions。您可以使用 catch{} 运算符来捕获异常并处理这些错误。

  1. 在 companion 对象中,实现一个不可变的 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(),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

  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。将 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
)
  1. 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
}
  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 初始化器 块中,使用以下代码获取 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> 的形式。此值必须映射到 UI 状态。

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

UserPreferencesRepository.isLinearLayout 是一个 Flow,它是 冷的。但是,为了向 UI 提供状态,最好使用热流,如 StateFlow,这样 UI 可以立即获得状态。

  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. 启动应用程序。注意,你可以点击切换图标在网格布局和线性布局之间切换。

b6e4bd0e50915b81.png 24a261db4cf2c6b8.png

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