第4章 DataStore存储
持久化这件事,我在项目中踩过不少坑。SharedPreferences 用起来方便,但一旦数据量大一点、跨进程读写,就各种玄学崩溃。后来全面转向 DataStore,才算真正稳下来。这一章咱们聊聊 DataStore 的两种形态——Preferences 和 Proto,以及怎么在 Compose 里优雅地用。
为什么我放弃了 SharedPreferences?
SharedPreferences 的问题其实很明显:
- apply 是异步的,但 commit 会卡 UI 线程
- 不支持类型安全,全靠 key-value 字符串硬编码
- 多进程场景下数据容易损坏
- 没有 Flow 响应式支持,需要自己包装
DataStore 基于 Flow 和协程,天生就是响应式架构的良配。说白了,它就是 Jetpack 官方用来替代 SharedPreferences 的组件。我个人习惯,只要是键值对配置(比如开关、用户偏好),一律用 Preferences DataStore;如果数据有固定结构(比如用户信息、同步配置),就用 Proto DataStore。
核心要点:DataStore 并非数据库替代品,它只适合轻量级配置和序列化数据。复杂的结构化数据还是交给 Room。
Preferences DataStore 实战
用法极其简单,适合存储用户主题、语言偏好、是否第一次启动这些标记。你想想看,以前用 SharedPreferences 还要写一堆 apply 回调,现在直接用 Flow 收集就行。
// 定义 DataStore
val Context.settingsDataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
// 写入键值
suspend fun saveDarkMode(enable: Boolean) {
settingsDataStore.edit { prefs ->
prefs[booleanPreferencesKey("dark_mode")] = enable
}
}
// 读取并观察
val darkModeFlow: Flow<Boolean> = settingsDataStore.data
.map { prefs ->
prefs[booleanPreferencesKey("dark_mode")] ?: false
}
小技巧:DataStore 的 edit 块内是事务性的,多个写入会合并成一次磁盘操作。但要注意,如果 edit 块里抛出异常,整个事务会回滚。我曾在日志里加了一个 NullPointerException,导致一整天用户的主题都恢复默认……嗯,后来加了 try-catch。
Proto DataStore 引入
当你的配置数据不再是孤立的键值,而是一组有结构的对象时——比如用户信息配置、网络代理配置——Preferences 就有点力不从心了。硬塞成多个 key 不仅难维护,而且容易忘掉某个字段。这时候就轮到 Proto DataStore 出场。
Proto DataStore 基于 Protocol Buffers 序列化。你需要先定义一个 .proto 文件,然后用 protobuf 插件编译出 Kotlin 类。听起来麻烦,但换来的是类型安全和序列化效率。
协议定义与序列化
举个例子,我们有个 UserSettings 结构:
// user_settings.proto
syntax = "proto3";
option java_package = "com.example.app.proto";
option java_multiple_files = true;
message UserSettings {
bool dark_mode = 1;
string display_name = 2;
int32 font_size = 3;
repeated string favorite_tags = 4;
}
然后在 build.gradle 里配置 protobuf 插件(这里不展开,文档写得很清楚)。编译后会自动生成 UserSettings 类。接下来:
// 序列化器
object UserSettingsSerializer : Serializer<UserSettings> {
override val defaultValue: UserSettings = UserSettings.getDefaultInstance()
override suspend fun readFrom(input: InputStream): UserSettings {
return UserSettings.parseFrom(input)
}
override suspend fun writeTo(t: UserSettings, output: OutputStream) {
t.writeTo(output)
}
}
// 定义 Proto DataStore
val Context.userSettingsDataStore: DataStore<UserSettings> by dataStore(
fileName = "user_settings.pb",
serializer = UserSettingsSerializer
)
在 Compose 中集成
DataStore 天然支持 Flow,与 Compose 的 collectAsState 配合起来非常丝滑。我个人习惯把读取逻辑封装到一个 ViewModel 里,避免 Compose 直接持有 DataStore 引用。
class SettingsViewModel(
private val dataStore: DataStore<Preferences>
) : ViewModel() {
val darkMode: StateFlow<Boolean> = dataStore.data
.map { it[booleanPreferencesKey("dark_mode")] ?: false }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false)
fun toggleDarkMode(enable: Boolean) {
viewModelScope.launch {
dataStore.edit { it[booleanPreferencesKey("dark_mode")] = enable }
}
}
}
// Compose 中使用
@Composable
fun ThemeToggle(viewModel: SettingsViewModel) {
val isDark by viewModel.darkMode.collectAsState()
Switch(
checked = isDark,
onCheckedChange = { viewModel.toggleDarkMode(it) }
)
}
这里有个细节要注意:WhileSubscribed(5000) 表示如果屏幕旋转等短暂配置变化,不会立即取消订阅,保留 5 秒缓存,避免不必要的重建。
避坑指南
我曾经在项目里同时跑 SharedPreferences 和 DataStore 迁移,结果忘记关掉老的 apply,两条写入路径互相覆盖。后来直接在 Application 里做一次性迁移,然后彻底移除旧代码。
- 迁移工具:DataStore 提供
SharedPreferencesMigration,但只支持 Preferences DataStore,不支持 Proto。如果需要从 SharedPreferences 迁移到 Proto,要自己写一个中间步骤。 - 文件大小时注意:Proto DataStore 文件可能随数据增长,如果频繁写入大量数据(比如日志),建议改用 Room。
- 多进程:DataStore 默认不支持多进程。如果一定要多进程读写,需要改用
MultiProcessDataStoreFactory,但官方不推荐,因为存在数据一致性问题。 - 测试:记得用
runTest配合Turbine测试 Flow,或者注入 InMemory DataStore 做单元测试。
警告:不要在 DataStore 里存储大型二进制对象(比如图片 base64),它会严重拖慢读写性能。这类数据请放 Room 或文件系统。
表格对比:Preferences vs Proto
| 维度 | Preferences DataStore | Proto DataStore |
|---|---|---|
| 数据结构 | 键值对,无类型约束 | 强类型,proto schema 定义 |
| 序列化 | 内部自动处理(String/Int/Boolean) | 需要自定义 Serializer,基于 protobuf |
| 灵活性 | 极高,任意 KV | 低,必须提前定义 |
| 性能 | 小数据量极快 | 中等,编解码有开销 |
| 典型场景 | 开关、偏好、简单计数 | 用户配置、序列化对象、多字段结构 |
结合实际项目,我的建议是:先用 Preferences 兜底,等发现需要字段版本管理、嵌套结构、或者需要与后端 proto 对齐时,再升级到 Proto DataStore。毕竟引入 protobuf 插件和编译配置还是有点成本的。不过一旦用上,那种类型安全带来的快感,真不是 SharedPreferences 能给的。