2. DataStore 核心概念:Preferences DataStore 与 Proto DataStore

2.1 两种 DataStore 的定位与适用场景

DataStore 分为 Preferences DataStore 和 Proto DataStore,二者均基于协程与 Flow,但数据模型与类型安全级别完全不同。

对比维度Preferences DataStoreProto DataStore
数据模型键值对 (Key-Value),仅支持基本类型 + Set<String>强类型 Protobuf 结构,支持嵌套对象、枚举、跨平台 schema
类型安全运行时类型校验,需 cast,易错编译期类型检查,自动生成 Java/Kotlin 类
序列化内置,无外部依赖需定义 .proto 文件,集成 Protobuf 插件与依赖
版本兼容手动处理兼容(无 schema)原生支持字段升级/降级,向前向后兼容
性能和体积轻量,无编译期开销稍重,需 Protobuf 依赖,但适合复杂结构化数据
推荐场景简单的用户偏好、开关、小量 Key-ValueApp 配置、用户数据、备份/同步的结构化数据
经验结论: 如果现有 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 协程。关键集成点:

  1. 读操作: val flow: Flow<DataType> = dataStore.data —— 每次文件变更自动发射新值。
  2. 写操作: suspend fun edit(transform: suspend (MutablePreferences) -> Unit) 或 suspend updateData { } 用于 Proto。
  3. 生命周期绑定: 配合 repeatOnLifecycle 或 .flowWithLifecycle() 在 UI 层收集,避免 Activity/Fragment 后台泄漏。
  4. 异常安全: 写操作内部捕获 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 数据流架构图

Preferences DataStore 与 Proto DataStore 数据流对比 Preferences DataStore Key-Value 映射 Int / Long / Float / Boolean / String edit {} 事务写 同步写 → 自动提交 Flow<Preferences> 读 协程挂起 / 响应式 无需 .proto 文件 运行时类型转换风险 Proto DataStore Protobuf 结构化消息 嵌套 / 枚举 / repeated updateData {} 事务写 完整消息对象替换 Flow<Message> 读 协程挂起 / 响应式 需定义 .proto + 编译插件 编译期类型安全 / 版本兼容 共用 协程 + Flow

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:

  1. 是否已有定义好的 Protobuf schema 或预期要跨平台同步?→ 是 → Proto DataStore
  2. 数据结构是否简单(基本类型 + 简单列表)且未来不改动?→ 是 → Preferences DataStore
  3. 是否需要字段版本迁移(增删字段)?→ 是 → Proto DataStore(内置)
  4. 团队是否熟悉 Protobuf 和代码生成?→ 是 → Proto DataStore
  5. 只想替换现有 SP,零学习成本?→ Preferences DataStore