第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。

DataStore 选型流程 开始选择 DataStore 数据结构是否固定且需要类型安全? 否 → 简单键值对 Preferences DataStore 是 → 需定义 Schema Proto DataStore 无需定义 .proto 文件 通过 prefs 直接存取 编写 .proto 定义结构 编译生成 Kotlin 类 两者都支持 Flow 响应式

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 能给的。