3. DataStore 实现与迁移:Preferences DataStore 与 Proto DataStore 的初始化、读写、异常处理、从 SharedPreferences 无缝迁移
DataStore 是 Jetpack 官方推荐的轻量级持久化方案,旨在替代传统的 SharedPreferences。它基于 Kotlin 协程与 Flow,天然支持异步操作与响应式订阅;同时提供 Preferences DataStore(键值对存储)和 Proto DataStore(类型化存储)两种实现。本章围绕初始化、读写、异常处理以及从 SharedPreferences 的安全迁移展开。
核心优势:DataStore 在 Android 主线程之外执行读写、避免 ANR;内置事务保证数据一致性;支持 Flow 实时监听数据变化。
3.1 Preferences DataStore 与 Proto DataStore 对比
| 维度 | Preferences DataStore | Proto DataStore |
|---|---|---|
| 数据模型 | 键值对(Key-Value) | 自定义 Protobuf Schema(类型化) |
| 类型安全 | 弱(需要手动序列化) | 强(编译期生成代码) |
| 依赖 | 仅 DataStore 库 | DataStore + Protobuf 插件 |
| Schema 变更 | 无约束,容易字段混乱 | 支持字段命名、默认值、向后兼容 |
| 适用场景 | 简单配置、开关、字符串 | 复杂结构化配置、多字段模型 |
3.2 Preferences DataStore 初始化与读写
3.2.1 添加依赖
// build.gradle.kts (Module: app)
implementation("androidx.datastore:datastore-preferences:1.1.1")
3.2.2 创建 DataStore 实例
推荐的初始化方式:在顶层扩展属性中创建单例,避免多次创建。
// 顶层扩展属性,延迟初始化
val Context.settingsDataStore: DataStore<Preferences> by preferencesDataStore(
name = "settings"
)
// 在 Activity 或 ViewModel 中使用
class MainViewModel(private val context: Context) : ViewModel() {
private val dataStore = context.settingsDataStore
}
3.2.3 写操作
suspend fun saveBoolean(key: Preferences.Key<Boolean>, value: Boolean) {
dataStore.edit { preferences ->
preferences[key] = value
}
}
// 使用
GlobalScope.launch {
dataStore.edit { prefs ->
prefs[PreferencesKeys.IS_DARK_MODE] = true
}
}
3.2.4 读操作(Flow 实时监听)
val darkModeFlow: Flow<Boolean> = dataStore.data.map { preferences ->
preferences[PreferencesKeys.IS_DARK_MODE] ?: false
}
// 在 Compose 或 Lifecycle 中 collect
lifecycleScope.launch {
dataStore.data.collect { prefs ->
val value = prefs[PreferencesKeys.USER_NAME] ?: "Guest"
updateUI(value)
}
}
3.3 Proto DataStore 初始化与读写
3.3.1 准备 Protobuf Schema
在 src/main/proto/ 目录下定义 .proto 文件,例如 user_settings.proto:
syntax = "proto3";
option java_package = "com.example.app";
option java_outer_classname = "UserSettingsProto";
message UserSettings {
bool is_dark_mode = 1;
string user_name = 2;
int32 font_size = 3;
}
3.3.2 配置 Protobuf 编译
在项目级 build.gradle.kts 中添加 Protobuf 插件,并在模块级配置:
// 项目级 build.gradle.kts
plugins {
id("com.google.protobuf") version "0.9.4" apply false
}
// 模块级 build.gradle.kts
plugins {
id("com.google.protobuf")
}
android {
// ...
sourceSets {
getByName("main") {
proto {
srcDir("src/main/proto")
}
}
}
}
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:3.25.1"
}
generateProtoTasks {
all().forEach { task ->
task.builtins {
create("java") {
option("lite")
}
}
}
}
}
dependencies {
implementation("androidx.datastore:datastore:1.1.1")
implementation("com.google.protobuf:protobuf-javalite:3.25.1")
}
3.3.3 创建 DataStore 实例
object UserSettingsSerializer : Serializer<UserSettings> {
override val defaultValue: UserSettings
get() = UserSettings.getDefaultInstance()
override suspend fun readFrom(input: InputStream): UserSettings {
try {
return UserSettings.parseFrom(input)
} catch (exception: InvalidProtocolBufferException) {
throw CorruptionException("Cannot read proto", exception)
}
}
override suspend fun writeTo(t: UserSettings, output: OutputStream) {
t.writeTo(output)
}
}
val Context.userSettingsDataStore: DataStore<UserSettings> by dataStore(
fileName = "user_settings.pb",
serializer = UserSettingsSerializer
)
3.3.4 读写操作
// 写
suspend fun updateSettings(isDarkMode: Boolean, userName: String) {
context.userSettingsDataStore.updateData { currentSettings ->
currentSettings.toBuilder()
.setIsDarkMode(isDarkMode)
.setUserName(userName)
.build()
}
}
// 读(Flow)
val settingsFlow: Flow<UserSettings> = context.userSettingsDataStore.data
// 在 ViewModel 中 collect
viewModelScope.launch {
context.userSettingsDataStore.data.collect { settings ->
// 处理 settings.isDarkMode / settings.userName
}
}
3.4 异常处理
DataStore 会捕获以下异常并暴露在 data Flow 中:
- IOException:文件读写失败(磁盘满、权限不足)→ Flow 会抛出异常,通常使用
catch操作符回退默认值。 - CorruptionException:Proto DataStore 解析数据格式错误(如手动篡改文件)→ 可清空文件并重置为默认值。
重要:不要在
edit 或 updateData 块中做耗时操作或抛未捕获异常,否则会导致 DataStore 进入错误状态并关闭,后续所有操作应创建新实例。建议在 collect 时使用 catch 保护 UI。
// 在 collect 时处理异常
lifecycleScope.launch {
context.settingsDataStore.data
.catch { exception ->
if (exception is IOException) {
Log.e("DataStore", "Read failed, using default", exception)
emit(emptyPreferences()) // 返回空 Preferences 作为降级
} else {
throw exception
}
}
.collect { prefs ->
// 安全使用
}
}
3.5 从 SharedPreferences 无缝迁移
DataStore 内置 SharedPreferencesMigration 类,支持在首次访问 DataStore 时自动将 SharedPreferences 中已有的数据迁移过来。
3.5.1 Preferences DataStore 迁移
val Context.migratingDataStore: DataStore<Preferences> by preferencesDataStore(
name = "migrated_settings",
produceMigrations = { context ->
listOf(SharedPreferencesMigration(context, "old_prefs_name"))
}
)
迁移逻辑:初始化后第一次读取 DataStore 时,若目标文件不存在,则读取旧 SharedPreferences 文件,将键值对逐条写入 DataStore,标记迁移完成,之后不再重复迁移。
3.5.2 Proto DataStore 迁移
需要提供转换函数,将 SharedPreferences 的 Map 转为 Proto 对象:
val Context.migratingProtoDataStore: DataStore<UserSettings> by dataStore(
fileName = "user_settings_migrated.pb",
serializer = UserSettingsSerializer,
produceMigrations = { context ->
listOf(
SharedPreferencesMigration(context, "old_prefs_name") { sharedPrefs: SharedPreferencesView, currentData: UserSettings ->
currentData.toBuilder()
.setIsDarkMode(sharedPrefs.getBoolean("is_dark_mode", false))
.setUserName(sharedPrefs.getString("user_name", "") ?: "")
.build()
}
)
}
)
3.5.3 迁移常见错误与处理
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 迁移后部分字段丢失 | 迁移转换函数未覆盖所有键 | 检查 SharedPreferences 中所有 key,并在 lambda 中逐一映射 |
| 迁移多次触发 | DataStore 实例创建多次,或清理缓存 | 确保单例;迁移成功后文件被创建,不会重复执行 |
| Proto DataStore 迁移时崩溃 | 旧 SharedPreferences 中存储了不兼容的值类型 | 在 lambda 中添加 try-catch,对异常值设置默认值 |
3.6 性能与稳定性优化建议
- 单例 DataStore:避免每次使用都创建新实例,导致重复迁移或文件冲突。
- 避免在主线程 collect:DataStore 读写自动在 Dispatchers.IO 执行,但
collect本身应在 LifecycleScope 或 ViewModelScope 中进行。 - 使用
map而不是collect多个字段:如果只需单个值,使用data.map { it[KEY] }减少不必要的数据拷贝。 - Proto 文件不要过大:DataStore 每次读写都会序列化/反序列化整个 Proto 对象,建议单条数据小于 100KB。
- 迁移后清理旧 SharedPreferences 文件:虽然 DataStore 不会自动删除,但旧文件不会影响功能,如需清理可在迁移成功回调中调用
sharedPrefs.edit().clear().apply()。
调试技巧:在初次开发时可以在
SharedPreferencesMigration 的 lambda 中打印日志,确认哪些键被迁移。使用 adb shell run-as com.example.app cat /data/data/com.example.app/files/datastore/xxx.preferences_pb 查看 DataStore 实际内容。