2. DataStore 核心概念:Preferences DataStore 与 Proto DataStore
2.1 两种 DataStore 的定位与适用场景
DataStore 分为 Preferences DataStore 和 Proto DataStore,二者均基于协程与 Flow,但数据模型与类型安全级别完全不同。
| 对比维度 | Preferences DataStore | Proto DataStore |
|---|---|---|
| 数据模型 | 键值对 (Key-Value),仅支持基本类型 + Set<String> | 强类型 Protobuf 结构,支持嵌套对象、枚举、跨平台 schema |
| 类型安全 | 运行时类型校验,需 cast,易错 | 编译期类型检查,自动生成 Java/Kotlin 类 |
| 序列化 | 内置,无外部依赖 | 需定义 .proto 文件,集成 Protobuf 插件与依赖 |
| 版本兼容 | 手动处理兼容(无 schema) | 原生支持字段升级/降级,向前向后兼容 |
| 性能和体积 | 轻量,无编译期开销 | 稍重,需 Protobuf 依赖,但适合复杂结构化数据 |
| 推荐场景 | 简单的用户偏好、开关、小量 Key-Value | App 配置、用户数据、备份/同步的结构化数据 |
经验结论: 如果现有 SharedPreferences 仅存储几个简单 key-value,直接迁移到 Preferences DataStore 成本最低;若存在多字段配置或远程同步需求,应优先 Proto DataStore 以获得类型安全与 schema 演进能力。
2.2 数据存储模型对比
2.2.1 Preferences DataStore 存储结构
内部以 Map<String, Object> 为模型,写入时序列化到本地文件(格式类似 JSON,但实际为二进制片段)。
- Key 必须为
Preferences.Key<T>,Value 仅支持Int、Long、Float、Double、Boolean、String、Set<String>。 - 读写通过
preferences.edit { }函数式块完成。
// Preferences DataStore 写入示例
val EXAMPLE_COUNTER = intPreferencesKey("example_counter")
context.dataStore.edit { preferences ->
preferences[EXAMPLE_COUNTER] = preferences[EXAMPLE_COUNTER] ?: 0 + 1
}
2.2.2 Proto DataStore 存储结构
基于 Protocol Buffers 的二进制格式,须先定义 .proto 消息结构。
- Schema 通过
.proto文件声明,编译后生成DataClass或 Java 类。 - 写入时需传入完整消息对象,支持嵌套、列表、枚举等复杂类型。
// user_preferences.proto
syntax = "proto3";
option java_package = "com.example.datastore";
option java_multiple_files = true;
message UserPreferences {
int32 theme = 1;
bool notifications_enabled = 2;
repeated string favorite_tags = 3;
}
// Rooms 配置 DataStore 时指定序列化器
object UserPreferencesSerializer : Serializer<UserPreferences> {
override val defaultValue: UserPreferences = UserPreferences.getDefaultInstance()
override suspend fun readFrom(input: InputStream): UserPreferences {
return UserPreferences.parseFrom(input)
}
override suspend fun writeTo(t: UserPreferences, output: OutputStream) {
t.writeTo(output)
}
}
2.3 协程集成模型
DataStore 所有读写操作均为挂起函数或以 Flow 形式暴露,天然融合 Kotlin 协程。关键集成点:
- 读操作:
val flow: Flow<DataType> = dataStore.data—— 每次文件变更自动发射新值。 - 写操作:
suspend fun edit(transform: suspend (MutablePreferences) -> Unit)或suspend updateData { }用于 Proto。 - 生命周期绑定: 配合
repeatOnLifecycle或.flowWithLifecycle()在 UI 层收集,避免 Activity/Fragment 后台泄漏。 - 异常安全: 写操作内部捕获
IOException并重试,但建议外层包try-catch或使用catch算子。
// 在 ViewModel 中收集 Flow
class SettingsViewModel(private val dataStore: DataStore<UserPreferences>) : ViewModel() {
val userPreferences: StateFlow<UserPreferences> = dataStore.data
.catch { exception ->
if (exception is IOException) {
emit(UserPreferences.getDefaultInstance())
} else {
throw exception
}
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = UserPreferences.getDefaultInstance()
)
}
常见错误: 在 UI 层直接调用挂起函数而不使用
lifecycleScope 或 viewModelScope,可能导致协程在 activity 重建后泄漏。务必收集 Flow 时绑定 Lifecycle.repeatOnLifecycle。2.4 数据流架构图
2.5 常见错误与稳定性优化
| 错误类别 | 现象 | 解决方案 |
|---|---|---|
| 写入未 suspend | “edit should be called from coroutine” 编译错误 | 确保调用在 coroutineScope / viewModelScope 内 |
| 读取未用 Flow | 读取到旧值或阻塞 UI | 改用 dataStore.data Flow 并 collect |
| Proto 定义变更未迁移 | 反序列化异常或默认值 | 使用 Protobuf 字段编号保留策略,避免删除字段 |
| 文件损坏不处理 | 首次读取抛 IOException 导致 crash | 在 .catch 中发射默认值,并后台删文件重建 |
| 磁盘 I/O 频繁 | 掉帧 / ANR(大量 edit 连续调用) | 合并写入操作到一次 edit 块;利用 debounce 算子 |
性能优化建议: 对于 Proto DataStore,避免在消息中存储超大 blob(如 bitmap),可改用文件路径或将大字段拆入独立 DataStore。Preferences DataStore 的 Set<String> 修改时需整体替换,频繁变化时应考虑独立的 Proto DataStore 存储。
2.6 调试技巧
- 查看本地文件: 设备上
/data/data/[package]/files/datastore/目录可找到对应文件,通过adb pull拉取分析。 - Proto 解码: 使用
protoc --decode_raw < xx.pb解码二进制文件(需根目录下 protoc 环境)。 - DataStore 日志: 开启 DataStore 内部日志
Preferences.DataStore = DEBUG;Proto 侧可通过自定义 Serializer 中的writeTo增加打印。 - Flaky test 模拟: 单元测试中注入 CoroutineDispatcher 或模拟 DataStore 抛出 IOException 测试异常恢复路径。
# adb 拉取 Preferences DataStore 文件
adb shell "run-as com.example.app cat /data/data/com.example.app/files/datastore/example.preferences_pb" > example.pb
# 通过 protoc 查看原始二进制(Proto DataStore)
protoc --decode_raw < example.pb
2.7 选择决策流程
以下流程可帮你快速决定当前项目应采用哪种 DataStore:
- 是否已有定义好的 Protobuf schema 或预期要跨平台同步?→ 是 → Proto DataStore
- 数据结构是否简单(基本类型 + 简单列表)且未来不改动?→ 是 → Preferences DataStore
- 是否需要字段版本迁移(增删字段)?→ 是 → Proto DataStore(内置)
- 团队是否熟悉 Protobuf 和代码生成?→ 是 → Proto DataStore
- 只想替换现有 SP,零学习成本?→ Preferences DataStore