18、协程与DataStore:DataStore的协程API、Preferences DataStore与Proto DataStore、Flow与DataStore的响应式读取、数据迁移与协程
说到Android本地存储,SharedPreferences 大家肯定都用过。但说实话,它在协程时代已经有点跟不上节奏了。SP 的同步读写会卡主线程,commit() 和 apply() 的行为又让人迷惑,更别提跨进程安全了。
Jetpack DataStore 就是来替代它的。它完全基于协程和 Flow 构建,天生就是响应式的。今天我就带大家把 DataStore 的协程玩法彻底捋一遍。
核心结论:DataStore = 协程 + Flow + 类型安全。它有两种实现:Preferences DataStore(键值对)和 Proto DataStore(带 Schema 的强类型存储)。
18.1 Preferences DataStore:轻量级键值存储
Preferences DataStore 用起来和 SharedPreferences 很像,但 API 完全是挂起函数和 Flow。我个人习惯用它存一些简单的配置项,比如用户是否首次启动、主题模式开关等。
先加依赖:
// build.gradle.kts (模块级)
implementation("androidx.datastore:datastore-preferences:1.1.1")
然后创建 DataStore 实例。我建议把它定义在顶层扩展属性里,方便全局访问:
// 顶层扩展
val Context.settingsDataStore: DataStore<Preferences> by preferencesDataStore(
name = "settings"
)
读写操作非常直观。写入时用 edit 挂起函数,读取时用 data 属性返回的 Flow:
// 写入(在协程中调用)
suspend fun saveThemeMode(isDark: Boolean) {
context.settingsDataStore.edit { preferences ->
preferences[THEME_MODE_KEY] = isDark
}
}
// 读取(返回 Flow)
val themeModeFlow: Flow<Boolean> = context.settingsDataStore.data.map { preferences ->
preferences[THEME_MODE_KEY] ?: false
}
// 伴生对象中定义 Key
companion object {
private val THEME_MODE_KEY = booleanPreferencesKey("theme_mode")
}
这里有个细节:edit 是挂起函数,内部会自动处理事务。如果写入过程中抛出异常,整个事务会回滚,数据不会损坏。我在项目中遇到过因为 SP 写入一半进程被杀导致数据丢失的情况,换成 DataStore 后就没再出过这种问题。
18.2 Proto DataStore:强类型存储
如果配置项比较复杂,比如有嵌套结构、枚举、列表,Preferences DataStore 就不够用了。这时候要用 Proto DataStore,它基于 Protocol Buffers 定义 Schema,数据是强类型的。
首先定义 proto 文件:
// app/src/main/proto/user_settings.proto
syntax = "proto3";
option java_package = "com.example.app";
option java_multiple_files = true;
message UserSettings {
bool is_dark_mode = 1;
int32 font_size = 2;
string display_name = 3;
}
然后在 build.gradle.kts 中配置 protobuf 插件:
plugins {
id("com.google.protobuf")
}
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:3.24.0"
}
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.24.0")
}
创建 DataStore 时,需要提供序列化器和默认值:
val Context.userSettingsDataStore: DataStore<UserSettings> by dataStore(
fileName = "user_settings.pb",
serializer = UserSettingsSerializer
)
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)
}
}
读写操作和 Preferences 类似,但操作的是具体的 proto 对象:
// 写入
suspend fun updateDisplayName(name: String) {
context.userSettingsDataStore.updateData { currentSettings ->
currentSettings.toBuilder()
.setDisplayName(name)
.build()
}
}
// 读取
val displayNameFlow: Flow<String> = context.userSettingsDataStore.data.map { settings ->
settings.displayName
}
我的建议:如果项目中有多个模块需要共享配置,或者配置项未来可能增加字段,直接用 Proto DataStore。虽然前期多写几行 proto 定义,但后期维护成本低很多。
18.3 Flow与DataStore的响应式读取
DataStore 最爽的地方就是它天然支持 Flow。你不需要手动注册监听器,也不需要担心生命周期。数据一变,Flow 就会发射新值。
来看一个实际场景:用户切换深色模式后,UI 要立刻响应。
// ViewModel 中
class SettingsViewModel(
private val dataStoreManager: DataStoreManager
) : ViewModel() {
val isDarkMode: StateFlow<Boolean> = dataStoreManager.themeModeFlow
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = false
)
fun toggleDarkMode() {
viewModelScope.launch {
dataStoreManager.saveThemeMode(!isDarkMode.value)
}
}
}
// Activity 中
lifecycleScope.launch {
viewModel.isDarkMode.collect { isDark ->
// 切换主题
AppCompatDelegate.setDefaultNightMode(
if (isDark) AppCompatDelegate.MODE_NIGHT_YES
else AppCompatDelegate.MODE_NIGHT_NO
)
}
}
这里我用了 SharingStarted.WhileSubscribed(5_000),意思是当订阅者数量降到 0 后,再等 5 秒才停止上游 Flow。这样做的好处是:如果用户只是旋转屏幕导致 Activity 重建,上游 Flow 不会中断,避免重复读取文件。
曾经踩过的坑:不要在 collect 里做耗时操作。DataStore 的 Flow 是在 Dispatchers.IO 上发射的,但 collect 所在的协程上下文取决于你。如果你在 Main 线程 collect,记得用 flowOn(Dispatchers.IO) 切换上游上下文。
18.4 数据迁移与协程
从 SharedPreferences 迁移到 DataStore 是很多项目要面对的问题。DataStore 官方提供了迁移支持,而且迁移逻辑也是挂起函数,完全协程化。
迁移的核心是 SharedPreferencesMigration 类:
val Context.settingsDataStore: DataStore<Preferences> by preferencesDataStore(
name = "settings",
produceMigrations = { context ->
listOf(
SharedPreferencesMigration(
context,
sharedPreferencesName = "old_settings"
) { sharedPrefs, currentData ->
// 迁移逻辑:将 SP 中的数据写入 DataStore
val isDark = sharedPrefs.getBoolean("is_dark_mode", false)
currentData.toMutablePreferences().apply {
this[booleanPreferencesKey("is_dark_mode")] = isDark
}
}
)
}
)
迁移过程是自动的。DataStore 首次读取数据时,会检查是否有未完成的迁移任务。如果有,它会按顺序执行迁移,然后将迁移后的数据写入新文件。整个过程在协程中异步完成,不会阻塞主线程。
对于 Proto DataStore,迁移逻辑类似,但需要自己处理 proto 对象的构建:
val Context.userSettingsDataStore: DataStore<UserSettings> by dataStore(
fileName = "user_settings.pb",
serializer = UserSettingsSerializer,
produceMigrations = { context ->
listOf(
SharedPreferencesMigration(
context,
sharedPreferencesName = "old_user_settings"
) { sharedPrefs, currentData ->
val builder = currentData.toBuilder()
builder.isDarkMode = sharedPrefs.getBoolean("is_dark_mode", false)
builder.fontSize = sharedPrefs.getInt("font_size", 16)
builder.build()
}
)
}
)
重要提醒:迁移完成后,旧的 SharedPreferences 文件不会被自动删除。如果你确定迁移成功,可以手动删除 SP 文件,或者用 sharedPrefs.edit().clear().apply() 清空数据。不过我个人建议保留一段时间,万一需要回滚呢?
18.5 知识体系总览
下面这张图总结了 DataStore 在协程体系中的位置和核心流程:
18.6 最佳实践总结
最后,我把 DataStore 在协程中的使用要点整理成表格,方便大家对照:
| 场景 | 推荐做法 | 避坑指南 |
|---|---|---|
| 简单键值对 | Preferences DataStore | Key 用 xxxPreferencesKey() 定义,不要硬编码字符串 |
| 复杂结构化数据 | Proto DataStore | proto 文件变更后要重新生成代码,注意向后兼容 |
| UI 响应式更新 | Flow + stateIn() | 用 WhileSubscribed 避免重复读取文件 |
| 从 SP 迁移 | SharedPreferencesMigration | 迁移完成后手动清理旧 SP 文件 |
| 测试 | 使用 DataStore<Preferences> 的 TestScope |
注入 Fake DataStore,不要依赖真实文件 |
个人习惯:我会在项目里封装一个 DataStoreManager 单例,统一管理所有 DataStore 实例和读写操作。这样 ViewModel 只依赖 Manager 接口,方便单元测试。
嗯,DataStore 的协程玩法就这些。它不像 Room 那么重,但比 SharedPreferences 强太多了。说白了,它就是协程时代本地存储的标配。你想想看,一个挂起函数写入、一个 Flow 读取,再配合自动迁移,还有什么理由不用它呢?