4. Proto DataStore 基础与 Protocol Buffers 定义
Proto DataStore 的核心是依靠 Protocol Buffers(Protobuf) 作为序列化框架。本章将带领你完成 .proto 文件编写、Schema 设计原则、生成 Java 类的方法,并展示 Proto DataStore 如何通过类型安全的 API 保证离线缓存的可靠性。
4.1 Proto DataStore 与 Protobuf 的关系
Proto DataStore 不是直接存储 Protobuf 二进制流,而是以 类型安全的 Message 对象 作为读写单位。开发者在 .proto 文件中定义数据模型,编译后生成对应的 Java 类,Proto DataStore 内部自动完成序列化/反序列化。
| 对比项 | Proto DataStore | Preferences DataStore |
|---|---|---|
| 序列化格式 | Protobuf(二进制紧凑) | 键值对(XML/文本) |
| 类型安全 | 强类型 Message 对象 | 弱类型(Key-String) |
| Schema 版本管理 | 内置字段编号、保留字段 | 无原生支持 |
| 延迟加载 | 支持 Flow 异步读取 | 同 |
| 数据大小 | 更节省空间 | 相对较大 |
| 迁移复杂度 | 需定义 proto、生成代码 | 简单但易出错 |
4.2 编写第一个 .proto 文件
以下示例定义用户偏好缓存的结构,包含主题、通知开关、字体大小三个字段。注意字段编号必须从 1 开始,不可重复,且后续不可修改编号含义。
// user_preferences.proto
syntax = "proto3";
package com.example.app.datastore;
option java_package = "com.example.app.datastore";
option java_multiple_files = true;
message UserPreferences {
string theme = 1; // 主题名称,如 "light"、"dark"
bool notifications_enabled = 2; // 是否开启通知
int32 font_size = 3; // 字体大小,单位为 sp
}
- 字段编号 1-15 使用 1 字节编码,应留给最频繁更新的字段(如
theme)。- 保留已删除的字段编号(使用
reserved)以防旧数据损坏。- 避免使用
repeated 字段存储大集合;若必须,考虑嵌套消息。
4.3 Schema 设计与类型安全规则
好的 Schema 是 Proto DataStore 迁移成功的一半。遵循以下原则可减少后期迁移成本:
- 单向增加字段:新版本只允许新增字段,不允许修改或删除已有字段含义。
- 默认值兼容:读取时若字段不存在,Protobuf 会返回类型默认值(string 为 "",int32 为 0,bool 为 false)。
- 字段编号不可重用:删除字段后使用
reserved将其编号标记为不可用。 - 使用
optional关键字(Proto3 支持)显式区分“未设置”与“默认值”。
在 Java/Kotlin 代码中直接引用字段编号(如
1、2)而不是通过生成的常量,会导致迁移时难以排查问题。应始终使用生成类中的字段访问器。
4.4 生成 Java 类
将 .proto 文件放置在 app/src/main/proto/ 目录下,并在模块的 build.gradle.kts 中添加 Protobuf 插件配置:
plugins {
id("com.google.protobuf") version "0.9.4"
}
android {
// ...
}
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:3.25.1"
}
generateProtoTasks {
all().forEach { task ->
task.builtins {
create("java") {
option("lite") // 使用 lite 库减少方法数
}
}
}
}
}
dependencies {
implementation("com.google.protobuf:protobuf-javalite:3.25.1")
implementation("androidx.datastore:datastore:1.1.1")
}
Gradle 同步并构建后,会在 build/generated/source/proto/ 下生成 UserPreferences.java 或 UserPreferencesOuterClass.java(取决于 java_multiple_files 设置)。
4.5 创建 Proto DataStore 实例
在 Kotlin 中,通过 createDataStore 扩展函数指定文件路径和序列化器:
// UserPreferencesSerializer.kt
object UserPreferencesSerializer : Serializer<UserPreferences> {
override val defaultValue: UserPreferences
get() = UserPreferences.getDefaultInstance()
override suspend fun readFrom(input: InputStream): UserPreferences {
return UserPreferences.parseFrom(input)
}
override suspend fun writeTo(t: UserPreferences, output: OutputStream) {
t.writeTo(output)
}
}
// 在 Application 或单例中创建 DataStore
private val Context.userPreferencesStore by preferencesDataStore(
fileName = "user_preferences.pb",
serializer = UserPreferencesSerializer
)
调用
userPreferencesStore.data 返回 Flow<UserPreferences>,所有字段通过生成类的方法访问(如 userPreferences.theme),编辑器会自动补全,编译时校验类型,从根本上杜绝了运行时类型转换错误。
4.6 Proto DataStore 读写流程结构
下图展示了从 .proto 定义到最终 Flow<Message> 的完整链路:
4.7 实战:写入与读取类型安全数据
以下示例展示使用 DataStore<UserPreferences> 更新主题字段:
// 写入(更新不可变消息的副本)
suspend fun updateTheme(context: Context, newTheme: String) {
context.userPreferencesStore.updateData { preferences ->
preferences.toBuilder()
.setTheme(newTheme)
.build()
}
}
// 读取(收集 Flow)
val userPreferencesFlow: Flow<UserPreferences> = context.userPreferencesStore.data
// 在 Compose 中使用
val theme by userPreferencesFlow.collectAsState(initial = UserPreferences.getDefaultInstance())
Text("当前主题:${theme.theme}")
-
updateData 是事务性的,冲突时自动重试。避免在 lambda 中执行耗时操作。- 若只需要读取,使用
.data 即可,不要频繁调用 updateData 做读操作。-
toBuilder() 每次创建新 Builder,适合小对象;大对象可考虑直接使用 newBuilder() 并从旧对象复制字段。
4.8 常见错误与调试方法
| 错误现象 | 可能原因 | 解决方式 |
|---|---|---|
java.lang.IllegalStateException: Serializer is not registered | Serializer 未通过 by preferencesDataStore 传给 DataStore | 检查文件创建时的 serializer 参数 |
com.google.protobuf.InvalidProtocolBufferException | 读取了格式不匹配的旧版本数据(Schema 不兼容) | 确保 proto 字段编号未改变,或使用 reserved 保留 |
编译报错:找不到 UserPreferences 类 | proto 文件路径或 Gradle protobuf 配置错误 | 检查 src/main/proto/ 目录,运行 Gradle sync |
| 运行时字段值为空/默认值 | 新字段在旧数据文件中未被写入 | 设计时应考虑默认值兼容,或迁移脚本 set 新字段默认值 |
通过本节的练习,你已经掌握了从 .proto 定义到类型安全 API 调用的完整路径。下一阶段的迁移工作将依赖这些基础组件。