第三章 Preferences DataStore 基础API与数据读写
本章聚焦 Preferences DataStore 的核心编程模型。你将掌握如何通过委托属性或工厂方法创建 DataStore 实例,如何进行同步/异步的读写操作,如何利用 Kotlin Flow 观察数据变化,以及如何正确处理文件读写中的常见异常。这些 API 是后续所有离线缓存迁移技术的基础。
3.1 依赖引入与项目配置
在 build.gradle.kts(模块级)中添加如下依赖:
dependencies {
// Preferences DataStore
implementation("androidx.datastore:datastore-preferences:1.1.1")
// 可选:如果使用 RxJava2 扩展
// implementation("androidx.datastore:datastore-preferences-rxjava2:1.1.1")
// 可选:如果使用 RxJava3 扩展
// implementation("androidx.datastore:datastore-preferences-rxjava3:1.1.1")
}
同步后即可使用 Preferences DataStore 的全部 API。
| 模块 | artifact | 版本 |
|---|---|---|
| 核心 | datastore-preferences | 1.1.1 |
| RxJava2 | datastore-preferences-rxjava2 | 1.1.1 |
| RxJava3 | datastore-preferences-rxjava3 | 1.1.1 |
3.2 创建 DataStore 实例
DataStore 实例推荐通过 Kotlin 顶层扩展属性创建,保证全局单例。标准做法是在文件顶部使用 preferencesDataStore 委托:
// 顶层扩展属性,全局唯一
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "user_prefs")
参数 name 指定文件名(实际文件为 user_prefs.preferences_pb)。若需要自定义序列化管理器或指定不同文件路径,可使用 DataStoreFactory:
val dataStore = DataStoreFactory.create(
serializer = PreferencesSerializer(),
produceFile = { context.filesDir.resolve("my_custom.prefs") }
)
private val 或 internal,避免在多个地方重复创建导致文件竞争。
3.3 写入数据:edit 与 transform
写入操作必须在协程作用域中调用,通常使用 DataStore.edit 挂起函数:
suspend fun saveUserName(name: String) {
context.dataStore.edit { preferences ->
preferences[USER_NAME_KEY] = name
}
}
private val USER_NAME_KEY = stringPreferencesKey("user_name")
如果需要原子性条件写入(先读取再决定是否写入),可使用 DataStore.transform 返回 Flow 并配合 collect 实现;但标准场景推荐 edit 配合 Preferences 的 MutablePreferences 操作。
- stringPreferencesKey — 存储 String 类型
- intPreferencesKey — 存储 Int 类型
- booleanPreferencesKey — 存储 Boolean 类型
- floatPreferencesKey — 存储 Float 类型
- longPreferencesKey — 存储 Long 类型
- doublePreferencesKey — 存储 Double 类型
所有 Preferences.Key<T> 必须定义为顶层 val 以避免重复创建。
3.4 读取数据:data Flow 与瞬时值
DataStore.data 属性返回 Flow<Preferences>,每次文件变更时自动发射新值:
val userNameFlow: Flow<String?> = context.dataStore.data
.map { preferences ->
preferences[USER_NAME_KEY]
}
// 在 ViewModel 或 Compose 中收集
viewModelScope.launch {
context.dataStore.data
.catch { exception ->
if (exception is IOException) {
emit(emptyPreferences()) // 恢复默认空值
} else {
throw exception
}
}
.map { it[USER_NAME_KEY] ?: "默认用户" }
.collect { name ->
// 更新 UI
}
}
如果需要获取一次性的当前值(非 Flow),可以使用 DataStore.data.first():
val currentName = context.dataStore.data.first()[USER_NAME_KEY]
.first() 是挂起函数,不要在主线中直接调用。DataStore 的读取是异步的,调用 .first() 会等待文件就绪。
3.5 异常处理策略
DataStore 的文件 I/O 可能抛出 IOException(如磁盘满、文件损坏)。推荐在 data Flow 中使用 catch 操作符,并在 edit 块使用 try-catch:
suspend fun safeWrite(key: Preferences.Key<String>, value: String) {
try {
context.dataStore.edit { prefs ->
prefs[key] = value
}
} catch (e: IOException) {
// 记录日志,可考虑重试或回退
Log.e("DataStore", "写入失败", e)
}
}
对于 Flow 读取,推荐模式:
context.dataStore.data
.catch { e ->
if (e is IOException) {
Log.w("DataStore", "读取异常,使用空 Preferences", e)
emit(emptyPreferences())
} else {
throw e
}
}
.map { preferences -> preferences[KEY] }
.collect { value -> /* 使用 value */ }
3.6 完整示例:读写与观察
一个简洁的 Repository 风格实现:
class UserPreferencesRepository(private val context: Context) {
private val dataStore = context.dataStore
companion object {
private val KEY_NICKNAME = stringPreferencesKey("nickname")
private val KEY_AGE = intPreferencesKey("age")
}
val nicknameFlow: Flow<String> = dataStore.data
.catch { emit(emptyPreferences()) }
.map { it[KEY_NICKNAME] ?: "未知" }
suspend fun updateNickname(nickname: String) {
dataStore.edit { prefs ->
prefs[KEY_NICKNAME] = nickname
}
}
suspend fun clearAll() {
dataStore.edit { it.clear() }
}
}
3.7 流程图:Preferences DataStore 基础操作流程
3.8 常见错误与调试
| 错误现象 | 可能原因 | 解决方案 |
|---|---|---|
| 写入后读取为旧值 | 未使用同一 DataStore 实例或 key 定义不一致 | 确保 key 是顶层 val 且使用相同 name 创建实例 |
java.io.IOException: Operation not permitted | 写入时磁盘权限不足或文件被锁定 | 检查存储权限(Android 10+ 无需额外权限),确保不在主线程调用 edit |
| Flow 不发射新值 | 未在 edit 中提交变更,或重复使用了 DataStore.data.first() 阻塞 | 确认 edit 块内修改了指定 key;使用 catch 处理异常后重新发射 |
| 应用重启后数据丢失 | 文件损坏或未等待写入完成 | 确保在 Kill 前调用过至少一次 edit 并等待完成;可添加 flush() 调试 |
3.9 性能与稳定性建议
- 最小化写入频率: 高频写入(如每 100ms)会导致大量序列化与磁盘 IO,应使用去抖动(debounce)合并操作。
- 避免在
edit中做复杂计算:transform块会持有写锁,耗时操作会阻塞其他读写。 - 使用
Dispatchers.IO调度 Flow: 默认DataStore.data的读取在Dispatchers.IO,但.map后的操作应显式指定调度器。 - 适当选择
first()vscollect: 仅需当前值用first(),持续监听用collect,避免内存泄漏。
DataStore.data 的 cachedSnapshot 调试属性查看当前缓存值(需 API 版本 1.1.0+)。或在 Logcat 中过滤 DataStore 标签观察文件操作日志。