写入基本类型数据:edit{}、putInt/putString 与 Flow 写入注意点

写入操作是 DataStore 偏好设置的核心能力。不同于 SharedPreferences 的直接同步写入,DataStore 采用基于 事务 的异步写入方式,通过挂起函数 edit{} 保证数据一致性。本章围绕原理、API 用法、代码示例、常见错误与性能要点展开。

4.1 写入原理:事务式编辑

DataStore 的写入发生在 DataStore<Preferences> 的 edit{} 扩展函数内。该函数接收一个挂起 lambda,lambda 内暴露 MutablePreferences 对象,调用其 putXxx() 方法完成写入。写入过程具有以下特征:

  • 原子性:整个 lambda 块被视为一个事务,要么全部成功,要么全部回滚。
  • 同步挂起:edit{} 是挂起函数,必须在协程中调用,写入操作不会阻塞 UI。
  • 序列化顺序:所有写入请求按照调用顺序排队执行,后一个事务会等待前一个完成。
核心对比: 与 SharedPreferences 的同步 edit().putXxx().apply() 不同,DataStore 的 edit{} 天然支持单线程写入,无需开发者手动加锁。Flow 在写入完成后自动发射新值。

4.2 实现流程:三步写入

  1. 获取 DataStore 实例:通过 preferencesDataStore 委托创建单例。
  2. 在协程作用域内调用 edit{}:使用 scope.launch 或 LifecycleScope。
  3. 调用对应 put 方法:putInt(key, value)、putString(key, value)、putBoolean(key, value)、putFloat(key, value)、putLong(key, value)、putDouble(key, value)、putStringSet(key, value)。
基本类型 put 方法 get 对应方法
IntputIntintPreferencesKey
StringputStringstringPreferencesKey
BooleanputBooleanbooleanPreferencesKey
FloatputFloatfloatPreferencesKey
LongputLonglongPreferencesKey
DoubleputDoubledoublePreferencesKey
Set<String>putStringSetstringSetPreferencesKey

4.3 关键 API 详解

4.3.1 edit{} 完整签名

suspend fun DataStore<Preferences>.edit(
    transform: suspend (MutablePreferences) -> Unit
): Preferences

transform 是挂起的 lambda,内部可调用任意 putXxx 方法。写入完成后返回新的 Preferences 对象。

4.3.2 写入基本类型示例

// 定义键(通常在顶层或伴生对象中)
val INT_KEY = intPreferencesKey("user_age")
val STRING_KEY = stringPreferencesKey("user_name")
val BOOL_KEY = booleanPreferencesKey("is_logged_in")

// 写入操作(在协程中执行)
suspend fun saveUserData(dataStore: DataStore<Preferences>) {
    dataStore.edit { preferences ->
        preferences[INT_KEY] = 28
        preferences[STRING_KEY] = "Alice"
        preferences[BOOL_KEY] = true
    }
}

注意:赋值语法 preferences[key] = value 是 putXxx 的简写形式,功能等价。

4.3.3 批量写入与条件写入

dataStore.edit { prefs ->
    // 仅当键不存在时写入
    val existing = prefs[STRING_KEY]
    if (existing == null) {
        prefs[STRING_KEY] = "default_name"
    }
    // 批量更新
    prefs[INT_KEY] = prefs[INT_KEY]?.plus(1) ?: 0
}

4.4 Flow 数据写入注意点

DataStore 将读取暴露为 Flow<Preferences> 或通过 data.data 获取。写入时需重点关注以下问题:

  • 读取与写入的生命周期分离:Flow 在收集期间自动监听磁盘变化,但写入必须主动调用 edit{},不会由 Flow 触发。
  • 不要在 Flow 的 collect 块内直接调用 edit{}:会导致重复写入嵌套,推荐将写入逻辑放在独立的 launch 中。
  • 写入后 Flow 自动更新:edit{} 提交后,DataStore 内部会写入文件并通知所有活跃的 Flow 收集器发射新值,无需手动刷新。
  • 避免高频写入:每调用一次 edit{} 都会触发射频文件同步,频繁调用可能影响性能。建议将多次修改合并到一个 edit{} 内。
常见错误: 在协程中调用 edit{} 时忘记处理异常,或使用非挂起上下文(如 GlobalScope)导致写入被忽略。务必使用适当的协程作用域(如 viewModelScope、lifecycleScope)。

4.5 写入流程内联 SVG

edit{} 写入事务流程 调用 edit { ... } 获取写入锁 执行 putInt / putString 等 序列化写入文件 释放锁 / 通知 Flow 发射 注意点 • 必须处于协程作用域 • 多次写入应合并一次 edit

4.6 调试与验证

写入后可通过读取 Flow 日志或使用 Android Studio 的 Device File Explorer 查看持久化文件:

// 查看文件位置(模拟器/设备)
// /data/data/包名/files/datastore/包名_preferences_pb

// 日志打印写入结果
dataStore.data.collect { preferences ->
    Log.d("DataStore", "age=${preferences[INT_KEY]}, name=${preferences[STRING_KEY]}")
}

4.7 性能与稳定性优化

  • 合并写入:将多个 put 放在同一个 edit{} 块内,减少文件 I/O 次数。
  • 避免大对象:putStringSet 存储大量字符串集合时注意序列化开销,建议限制单个 value 大小在 1MB 以下。
  • 异常处理:对 edit{} 使用 try-catch 捕获 IOException,防止写过程中断导致数据丢失。
dataStore.edit { prefs ->
    prefs[INT_KEY] = newValue
    // 潜在异常:文件写满、磁盘故障等
}.also { result ->
    // 成功写入
}.catch { e ->
    Log.e("DataStore", "写入失败", e)
    // 可考虑重试或回滚逻辑
}
最佳实践: 在 ViewModel 中使用 viewModelScope.launch 调用 edit{},自动跟随 Activity/Fragment 生命周期。UI 层仅负责读取 Flow,不直接写入。