第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 实现流程

  1. 在 build.gradle 中添加 DataStore 依赖。
  2. 选择实现类型:若为简单键值存储,使用 Preferences DataStore;若需结构化数据并保证 Schema 演进,选择 Proto DataStore。
  3. 定义存储结构并创建 DataStore 实例(单例或通过依赖注入)。
  4. 编写 Repository / UseCase 层,通过 edit 或 updateData 进行异步写操作,通过 data Flow 读数据。
  5. 实现迁移策略:从 SharedPreferences 迁移或 DataStore 版本之间的 Schema 迁移。
架构示意图:以下 SVG 展示了 DataStore 在 Android 应用中的位置及数据流动路径。
DataStore 架构与数据流 Android 应用 Repository / ViewModel DataStore API Preferences / Proto 序列化 (Serializer) Proto / JSON 持久化文件 .preferences_pb / .datastore SharedPreferences 迁移源 业务层 API层 序列化 文件存储 迁移源

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 可能导致序列化性能下降。
  • 错误处理:在 data Flow 中添加 catch 处理序列化异常,返回默认值避免崩溃。
警告:不要在 main 线程同步调用 DataStore 的 data 或 edit,会导致 ANR。确保所有操作在协程或子线程中执行。

3.10 本章总结

Preferences DataStore 适合简单键值对场景,配置简单;Proto DataStore 适合需要结构化数据、Schema 演进和强类型的场景。无论哪种实现,都建议遵循单例、异步、流式数据观察的原则。迁移策略是上线前必须考虑的部分,尤其是老项目从 SharedPreferences 过渡时,使用内置的 SharedPreferencesMigration 可以平滑过渡。

下一阶段将学习如何将 DataStore 与 WorkManager 结合,实现带有缓存策略的后台数据持久化任务。