美文网首页人生几何?
Jetpack(七) 之 Data Store

Jetpack(七) 之 Data Store

作者: zcwfeng | 来源:发表于2021-09-15 14:41 被阅读0次

    您可能经常需要存储较小或简单的数据集。为此,您过去可能使用过 SharedPreferences,但此 API 也存在一系列缺点。Jetpack DataStore 库旨在解决这些问题,从而创建一个简单、安全性更高的异步 API 来存储数据。它提供 2 种不同的实现:

    • Preferences DataStore

    • Proto DataStore

    功能 SharedPreferences PreferencesDataStore ProtoDataStore
    异步 API ✅(仅用于通过监听器读取已更改的值) ✅(通过 Flow ✅(通过 Flow
    同步 API ✅(但在界面线程中调用不安全)
    可在界面线程上安全调用 ❌* ✅(这项工作已在后台移至 Dispatchers.IO ✅(这项工作已在后台移至 Dispatchers.IO
    可以提示错误
    避免运行时异常 ❌**
    包含具有强一致性保证的事务性 API
    处理数据迁移 ✅(迁移自 SharedPreferences) ✅(迁移自 SharedPreferences)
    类型安全 ✅ 使用协议缓冲区
    • SharedPreferences 有一个看上去可以在界面线程中安全调用的同步 API,但是该 API 实际上执行磁盘 I/O 操作。此外,apply() 会阻塞 fsync() 上的界面线程。每次服务开始或停止以及 activity 在应用中的任何地方启动或停止时,系统都会触发待处理的 fsync() 调用。界面线程在 apply() 调度的待处理 fsync() 调用上会被阻塞,这通常会导致 ANR

    ** SharedPreferences 将解析错误作为运行时异常抛出。

    Preferences DataStore 与 Proto DataStore

    虽然 Preferences DataStore 和 Proto DataStore 都允许保存数据,但它们保存数据的方式不同:

    • 与 SharedPreferences 一样,Preference DataStore 可以根据键访问数据,而无需事先定义架构。

    • Proto DataStore 使用协议缓冲区定义架构。使用 Protobuf 可存留强类型数据。与 XML 和其他类似的数据格式相比,它们速度更快、规格更小、使用更简单,并且更清楚明了。虽然使用 Proto DataStore 需要学习新的序列化机制,但我们认为 Proto DataStore 有着强大的优势,值得去学习。

    Room 与 DataStore

    如果您需要实现部分更新、引用完整性或大型/复杂数据集,您应考虑使用 Room,而不是 DataStore。DataStore 非常适合较小或简单的数据集,而不支持部分更新或引用完整性。

    [ Preferences DataStore 概览]

    Preference DataStore API 类似于 SharedPreferences,但与后者相比存在一些显著差异:

    • 以事务方式处理数据更新
    • 公开表示当前数据状态的 Flow
    • 不提供存留数据的方法(apply()commit()
    • 不返回对其内部状态的可变引用
    • 通过类型化键提供类似于 MapMutableMap 的 API

    接下来我们看看如何将其添加到项目中,并将 SharedPreferences 迁移到 DataStore。

    添加依赖项

    更新 build.gradle 文件以添加以下 Preference DataStore 依赖项:

    implementation "androidx.datastore:datastore-preferences:1.0.0-alpha06"
    

    [在 Preferences DataStore 中存留数据]

    尽管显示已完成标志和排序顺序标志都是用户偏好设置,但它们现在用两种不同的对象表示。因此,我们的一个目标是在 UserPreferences 类中整合这两个标志,并使用 DataStore 将其存储在 UserPreferencesRepository 中。目前,显示已完成标志保存在内存的 TasksViewModel 中。

    首先,在 UserPreferencesRepository 中创建 UserPreferences 数据类。目前,它应该只有一个字段:showCompleted。稍后我们将添加排序顺序。

    data class UserPreferences(val showCompleted: Boolean)
    

    创建 DataStore

    我们使用 context.createDataStoreFactory() 方法在 UserPreferencesRepository 中创建 DataStore<Preferences> 私有字段。必需的参数是 Preferences DataStore 的名称。

    private val dataStore: DataStore<Preferences> =
            context.createDataStore(name = "user")
    

    从 Preferences DataStore 读取数据

    Preferences DataStore 公开 Flow<Preferences> 中存储的数据,每当偏好设置发生变化时,Flow<Preferences> 就会发出该数据。我们不希望公开整个 Preferences 对象,而是要公开 UserPreferences 对象。为此,我们必须映射 Flow<Preferences>,根据键获取感兴趣的布尔值,并构造一个 UserPreferences 对象。

    因此,我们首先需要定义 show completed 键,这是一个 booleanPreferencesKey 值,我们将其声明为私有 PreferencesKeys 对象中的成员。

    private object PreferencesKeys {
      val SHOW_COMPLETED = booleanPreferencesKey("show_completed")
    }
    

    我们将公开一个基于 dataStore.data: Flow<Preferences> 构造的 userPreferencesFlow: Flow<UserPreferences>,然后将其映射,以检索正确的偏好设置:

    从 Preferences DataStore 读取数据

    Preferences DataStore 公开 Flow<Preferences> 中存储的数据,每当偏好设置发生变化时,Flow<Preferences> 就会发出该数据。我们不希望公开整个 Preferences 对象,而是要公开 UserPreferences 对象。为此,我们必须映射 Flow<Preferences>,根据键获取感兴趣的布尔值,并构造一个 UserPreferences 对象。

    因此,我们首先需要定义 show completed 键,这是一个 booleanPreferencesKey 值,我们将其声明为私有 PreferencesKeys 对象中的成员。

    private object PreferencesKeys {
      val SHOW_COMPLETED = booleanPreferencesKey("show_completed")
    }
    

    我们将公开一个基于 dataStore.data: Flow<Preferences> 构造的 userPreferencesFlow: Flow<UserPreferences>,然后将其映射,以检索正确的偏好设置:

    val userPreferencesFlow: Flow<UserPreferences> = dataStore.data
        .map { preferences ->
            // Get our show completed value, defaulting to false if not set:
            val showCompleted = preferences[PreferencesKeys.SHOW_COMPLETED]?: false
            UserPreferences(showCompleted)
        }
    

    处理读取数据时的异常

    当 DataStore 从文件读取数据时,如果读取数据期间出现错误,系统会抛出 IOExceptions。我们可以通过以下方式处理这些事务:在 map() 之前使用 catch() Flow 运算符,并且在抛出的异常是 IOException 时发出 emptyPreferences()。如果出现其他类型的异常,最好重新抛出该异常。

    val userPreferencesFlow: Flow<UserPreferences> = dataStore.data
        .catch { exception ->
            // dataStore.data throws an IOException when an error is encountered when reading data
            if (exception is IOException) {
                emit(emptyPreferences())
            } else {
                throw exception
            }
        }.map { preferences ->
            // Get our show completed value, defaulting to false if not set:
            val showCompleted = preferences[PreferencesKeys.SHOW_COMPLETED]?: false
            UserPreferences(showCompleted)
        }
    

    将数据写入 Preferences DataStore

    如需写入数据,DataStore 提供挂起 DataStore.edit(transform: suspend (MutablePreferences) -> Unit) 函数,该函数接受 transform 块,让我们能够以事务方式更新 DataStore 中的状态。

    传递给转换块的 MutablePreferences 将保持以前所有运行编辑的最新状态。在 transform 完成后且 edit 完成之前,对 transform 块中 MutablePreferences 的所有更改都将应用于磁盘。在 MutablePreferences 中设置一个值会使所有其他偏好设置保持不变。

    注意:请勿尝试修改转换块之外的 MutablePreferences

    现在我们来创建一个挂起函数,以便我们能够更新 UserPreferencesshowCompleted 属性,此函数称为 updateShowCompleted(),用于调用 dataStore.edit() 并设置新值:

    suspend fun updateShowCompleted(showCompleted: Boolean) {
        dataStore.edit { preferences ->
            preferences[PreferencesKeys.SHOW_COMPLETED] = showCompleted
        }
    }
    

    如果在读取或写入磁盘时发生错误,edit() 可能会抛出 IOException。如果转换块中出现任何其他错误,edit() 将抛出异常。

    此时,应用应该编译,但是在 UserPreferencesRepository 中创建的功能并未使用。

    [从 SharedPreferences 迁移到 Preferences DataStore]

    排序顺序保存在 SharedPreferences 中。让我们将其迁移到 DataStore 中。为此,让我们先更新 UserPreferences 以存储排序顺序:

    data class UserPreferences(
        val showCompleted: Boolean,
        val sortOrder: SortOrder
    )
    

    从 SharedPreferences 迁移

    为了能够将排序顺序迁移到 DataStore,我们需要更新 DataStore 构建器以向迁移列表传递 SharedPreferencesMigration。DataStore 能够自动从 SharedPreferences 迁移到 DataStore。迁移可在 DataStore 中进行任何数据访问之前运行。这意味着,必须在 DataStore.data 发出任何值之前和 DataStore.edit() 可以更新数据之前,成功完成迁移。

    注意:由于键只能从 SharedPreferences 迁移一次,因此在代码迁移到 DataStore 之后,您应停止使用旧 SharedPreferences。

    private val dataStore: DataStore<Preferences> =
        context.createDataStore(
            name = USER_PREFERENCES_NAME,
            migrations = listOf(SharedPreferencesMigration(context, USER_PREFERENCES_NAME))
        )
    
    private object PreferencesKeys {
        ...
        // Note: this has the the same name that we used with SharedPreferences.
        val SORT_ORDER = stringPreferencesKey("sort_order")
    }
    

    所有键都将迁移到我们的 DataStore,并从用户偏好设置 SharedPreferences 中删除。现在,我们可以从 Preferences 获取并更新基于 SORT_ORDER 键的 SortOrder

    从 DataStore 中读取排序顺序

    现在,让我们更新 userPreferencesFlow 以同时检索 map() 转换中的排序顺序:

    val userPreferencesFlow: Flow<UserPreferences> = dataStore.data
        .catch { exception ->
            if (exception is IOException) {
                emit(emptyPreferences())
            } else {
                throw exception
            }
        }.map { preferences ->
            // Get the sort order from preferences and convert it to a [SortOrder] object
            val sortOrder =
                SortOrder.valueOf(
                    preferences[PreferencesKeys.SORT_ORDER] ?: SortOrder.NONE.name)
    
            // Get our show completed value, defaulting to false if not set:
            val showCompleted = preferences[PreferencesKeys.SHOW_COMPLETED] ?: false
            UserPreferences(showCompleted, sortOrder)
        }
    

    将排序顺序保存到 DataStore

    目前,UserPreferencesRepository 仅公开一种用于设置排序顺序标志的同步方法,并且存在并发问题。我们公开了两种更新排序顺序的方法:enableSortByDeadline()enableSortByPriority();这两种方法都依赖当前的排序顺序值,但如果在一个方法结束之前调用另一个方法,则最终值可能会出错。

    由于 Datastore 保证以事务方式进行数据更新,所以我们不会再遇到这个问题。接下来,让我们一起执行以下更改:

    • enableSortByDeadline()enableSortByPriority() 更新为使用 dataStore.edit()suspend 函数。
    • edit() 的转换块中,我们将从 Preferences 参数中获取 currentOrder,而不是从 _sortOrderFlow 字段中进行检索。
    • 我们可以直接在偏好设置中更新排序顺序,而不是调用 updateSortOrder(newSortOrder)

    具体实现如下所示。

    suspend fun enableSortByDeadline(enable: Boolean) {
        // edit handles data transactionally, ensuring that if the sort is updated at the same
        // time from another thread, we won't have conflicts
        dataStore.edit { preferences ->
            // Get the current SortOrder as an enum
            val currentOrder = SortOrder.valueOf(
                preferences[PreferencesKeys.SORT_ORDER] ?: SortOrder.NONE.name
            )
    
            val newSortOrder =
                if (enable) {
                    if (currentOrder == SortOrder.BY_PRIORITY) {
                        SortOrder.BY_DEADLINE_AND_PRIORITY
                    } else {
                        SortOrder.BY_DEADLINE
                    }
                } else {
                    if (currentOrder == SortOrder.BY_DEADLINE_AND_PRIORITY) {
                        SortOrder.BY_PRIORITY
                    } else {
                        SortOrder.NONE
                    }
                }
            preferences[PreferencesKeys.SORT_ORDER] = newSortOrder.name
        }
    }
    
    suspend fun enableSortByPriority(enable: Boolean) {
        // edit handles data transactionally, ensuring that if the sort is updated at the same
        // time from another thread, we won't have conflicts
        dataStore.edit { preferences ->
            // Get the current SortOrder as an enum
            val currentOrder = SortOrder.valueOf(
                preferences[PreferencesKeys.SORT_ORDER] ?: SortOrder.NONE.name
            )
    
            val newSortOrder =
                if (enable) {
                    if (currentOrder == SortOrder.BY_DEADLINE) {
                        SortOrder.BY_DEADLINE_AND_PRIORITY
                    } else {
                        SortOrder.BY_PRIORITY
                    }
                } else {
                    if (currentOrder == SortOrder.BY_DEADLINE_AND_PRIORITY) {
                        SortOrder.BY_DEADLINE
                    } else {
                        SortOrder.NONE
                    }
                }
            preferences[PreferencesKeys.SORT_ORDER] = newSortOrder.name
        }
    }
    

    [更新 TasksViewModel 以使用 UserPreferencesRepository]

    现在,UserPreferencesRepository 在 DataStore 中存储了显示已完成标志和排序顺序标志,并公开了 Flow<UserPreferences>。接下来,让我们更新并使用 TasksViewModel

    移除 showCompletedFlowsortOrderFlow,创建一个名为 userPreferencesFlow 的值,用 userPreferencesRepository.userPreferencesFlow 对该值进行初始化:

    private val userPreferencesFlow = userPreferencesRepository.userPreferencesFlow
    

    tasksUiModelFlow 创建中,将 showCompletedFlowsortOrderFlow 替换为 userPreferencesFlow。请相应地替换参数。

    调用 filterSortTasks 时,传入 userPreferencesshowCompletedsortOrder。您的代码应如下所示:

    private val tasksUiModelFlow = combine(
            repository.tasks,
            userPreferencesFlow
        ) { tasks: List<Task>, userPreferences: UserPreferences ->
            return@combine TasksUiModel(
                tasks = filterSortTasks(
                    tasks,
                    userPreferences.showCompleted,
                    userPreferences.sortOrder
                ),
                showCompleted = userPreferences.showCompleted,
                sortOrder = userPreferences.sortOrder
            )
        }
    

    现在,showCompletedTasks() 函数应更新为调用 userPreferencesRepository.updateShowCompleted()。由于该函数为挂起函数,因此请在 viewModelScope 中创建新的协程:

    fun showCompletedTasks(show: Boolean) {
        viewModelScope.launch {
            userPreferencesRepository.updateShowCompleted(show)
        }
    }
    

    userPreferencesRepository 函数 enableSortByDeadline()enableSortByPriority() 现在属于挂起函数,因此还应在 viewModelScope 中启动的新协程中调用它们:

    fun enableSortByDeadline(enable: Boolean) {
        viewModelScope.launch {
           userPreferencesRepository.enableSortByDeadline(enable)
        }
    }
    
    fun enableSortByPriority(enable: Boolean) {
        viewModelScope.launch {
            userPreferencesRepository.enableSortByPriority(enable)
        }
    }
    

    清理 UserPreferencesRepository

    让我们移除不再需要的字段和方法。您应该可以删除以下内容:

    • _sortOrderFlow
    • sortOrderFlow
    • updateSortOrder()
    • private val sortOrder: SortOrder

    我们的应用现在应能成功编译。运行一下,看看“显示已完成”标志和排序顺序标志是否保存成功。

    相关文章

      网友评论

        本文标题:Jetpack(七) 之 Data Store

        本文链接:https://www.haomeiwen.com/subject/kdwkgltx.html