第3章 DataStore 实现键值持久化
3.1 原理概述
Jetpack DataStore 是 Google 推出的新一代本地键值存储方案,用于替代 SharedPreferences。它基于 Kotlin 协程和 Flow 提供异步、一致且类型安全的读写能力,彻底解决了 SharedPreferences 在同步读写、ANR 风险、类型不安全等方面的痛点。
DataStore 提供两种实现方式:
- Preferences DataStore:存储键值对,类似 SharedPreferences 但完全异步。
- Proto DataStore:存储自定义 Proto 对象,利用 Protocol Buffers 序列化保证类型安全与向后兼容。
两种实现均依赖单一数据源,并通过 Flow<T> 暴露数据,支持数据变更观察与流式处理。
3.2 实现流程
- 在
build.gradle中添加 DataStore 依赖。 - 选择实现类型:若为简单键值存储,使用 Preferences DataStore;若需结构化数据并保证 Schema 演进,选择 Proto DataStore。
- 定义存储结构并创建 DataStore 实例(单例或通过依赖注入)。
- 编写 Repository / UseCase 层,通过
edit或updateData进行异步写操作,通过dataFlow 读数据。 - 实现迁移策略:从 SharedPreferences 迁移或 DataStore 版本之间的 Schema 迁移。
架构示意图:以下 SVG 展示了 DataStore 在 Android 应用中的位置及数据流动路径。
3.3 配置与依赖
在项目级 build.gradle 的 dependencies 中添加:
implementation("androidx.datastore:datastore-preferences:1.1.1")
// 若使用 Proto DataStore
implementation("androidx.datastore:datastore:1.1.1")
implementation("com.google.protobuf:protobuf-javalite:3.25.1")
3.4 Preferences DataStore 实现
3.4.1 创建实例
通常在顶层属性或 Application 中创建单例:
val Context.settingsDataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
读取:
val USER_NAME_KEY = stringPreferencesKey("user_name")
val userFlow: Flow<String?> = settingsDataStore.data.map { prefs ->
prefs[USER_NAME_KEY]
}
写入(需在协程中):
suspend fun saveUserName(name: String) {
settingsDataStore.edit { prefs ->
prefs[USER_NAME_KEY] = name
}
}
3.5 Proto DataStore 实现
3.5.1 定义 .proto 文件
在 app/src/main/proto 目录下创建 user_settings.proto:
syntax = "proto3";
option java_package = "com.example.app";
option java_multiple_files = true;
message UserSettings {
string user_name = 1;
bool is_logged_in = 2;
int32 theme = 3;
}
3.5.2 配置 Protobuf 生成
在 build.gradle 中添加 protobuf 插件:
plugins {
id "com.google.protobuf" version "0.9.4"
}
protobuf {
protoc { artifact = "com.google.protobuf:protoc:3.25.1" }
generateProtoTasks {
all().each { task ->
task.builtins {
java { option 'lite' }
}
}
}
}
3.5.3 创建 Serializer 类
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)
}
}
3.5.4 创建 DataStore 实例与读写
val Context.userSettingsDataStore: DataStore<UserSettings> by dataStore(
fileName = "user_settings.pb",
serializer = UserSettingsSerializer
)
// 读
val userSettingsFlow: Flow<UserSettings> = userSettingsDataStore.data
// 写
suspend fun updateUserName(name: String) {
userSettingsDataStore.updateData { currentSettings ->
currentSettings.toBuilder().setUserName(name).build()
}
}
3.6 迁移策略
从 SharedPreferences 迁移到 DataStore 是最常见的场景,DataStore 内置支持:
val SharedPreferences.Migration = SharedPreferencesMigration(
sharedPreferencesName = "shared_prefs_name",
keysToMigrate = setOf("key1", "key2")
)
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
name = "app_settings",
produceMigrations = { context ->
listOf(SharedPreferencesMigration(context, "my_shared_prefs"))
}
)
对于 Proto DataStore 的 Schema 变更,需要实现自定义迁移:
object MyMigration : DataMigration<UserSettings> {
override suspend fun shouldMigrate(currentData: UserSettings): Boolean {
return currentData.version < 2
}
override suspend fun migrate(currentData: UserSettings): UserSettings {
return currentData.toBuilder().apply {
if (!hasSomeNewField()) setSomeNewField("default")
}.build()
}
override suspend fun cleanUp() {
// 清理旧缓存
}
}
val Context.dataStore: DataStore<UserSettings> by dataStore(
fileName = "user_settings.pb",
serializer = UserSettingsSerializer,
dataMigrations = listOf(MyMigration)
)
3.7 调试与测试
- Android Studio Database Inspector:可以查看
.datastore文件内容,但需要应用在调试模式运行。 - 日志输出:为 DataStore 设置
android:debug标签可观察操作日志,例如使用Log.d在自定义 Serializer 中打印读写字节数。 - 单元测试:使用
runTest和FakeDataStore模拟协程与 Flow,避免真实文件操作。
调试技巧:在开发阶段可以添加一个
collect 块打印每次数据变更,快速验证迁移是否生效。
3.8 常见错误
| 错误 | 原因 | 解决 |
|---|---|---|
UninitializedPropertyAccessException |
在 DataStore 实例创建前读取 data |
确保委托属性初始化后再访问,使用 by lazy 或依赖注入 |
| 协程作用域泄漏 | 在 ViewModel 外使用 lifecycleScope |
使用 viewModelScope 或 GlobalScope(不建议长期用) |
| Proto 序列化失败(InvalidProtocolBufferException) | 文件损坏或 Schema 不兼容 | 添加正确的迁移策略,或在第一次读取失败时使用默认值 |
| 写入未生效 | 忘记调用 edit 或 updateData |
确保写入操作在协程中且使用了 DataStore 提供的写方法 |
3.9 性能与稳定性优化
- 单例模式:整个应用只应持有一个 DataStore 实例,避免重复创建导致文件锁冲突。
- 减少频繁写入:合并多次写入操作,使用 debounce 或批次处理。
- 合理使用 Flow:利用
distinctUntilChanged减少不必要的 UI 刷新。 - 文件大小控制:Proto DataStore 建议控制在几 MB 以内,过大的 message 可能导致序列化性能下降。
- 错误处理:在
dataFlow 中添加catch处理序列化异常,返回默认值避免崩溃。
警告:不要在
main 线程同步调用 DataStore 的 data 或 edit,会导致 ANR。确保所有操作在协程或子线程中执行。
3.10 本章总结
Preferences DataStore 适合简单键值对场景,配置简单;Proto DataStore 适合需要结构化数据、Schema 演进和强类型的场景。无论哪种实现,都建议遵循单例、异步、流式数据观察的原则。迁移策略是上线前必须考虑的部分,尤其是老项目从 SharedPreferences 过渡时,使用内置的 SharedPreferencesMigration 可以平滑过渡。
下一阶段将学习如何将 DataStore 与 WorkManager 结合,实现带有缓存策略的后台数据持久化任务。