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 DataStoreProto DataStore
数据模型键值对(Key-Value)自定义 Protobuf Schema(类型化)
类型安全弱(需要手动序列化)强(编译期生成代码)
依赖仅 DataStore 库DataStore + Protobuf 插件
Schema 变更无约束,容易字段混乱支持字段命名、默认值、向后兼容
适用场景简单配置、开关、字符串复杂结构化配置、多字段模型
从 SharedPreferences 迁移到 DataStore 流程图 SharedPreferences 选择 DataStore 类型 (根据数据结构) Preferences DataStore 简单键值对 Proto DataStore 自定义 Schema 实现 SharedPreferences → DataStore 迁移 提示:无论是 Preferences 还是 Proto,迁移逻辑都封装在 SharedPreferencesMigration 中,DataStore 自动处理旧数据转换。

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 实际内容。