13、Flow与DataStore:DataStore的Flow支持、替代SharedPreferences、类型安全的数据存储
说实话,每次看到项目里还在用SharedPreferences存复杂对象,我都替那帮开发者捏把汗。我自己早期维护过一个日活百万的App,SharedPreferences里塞了上百个key-value,每次读写都卡主线程,ANR报告里一半都是它惹的祸。后来换成DataStore,世界清净了。
这一章,我们就来聊聊DataStore。它本质上是Google官方推出的、基于Flow的、类型安全的数据存储方案。说白了,就是用来替代SharedPreferences的。
核心结论:DataStore + Flow = 响应式、协程安全、类型安全的本地键值存储。别再写SharedPreferences了。
13.1 为什么SharedPreferences该退休了?
SharedPreferences有几个硬伤,我简单列一下:
- 同步读写阻塞主线程——你调用getString()时,它可能触发磁盘IO,直接卡UI。
- 没有类型安全——存的时候是String,取的时候可能强转失败,运行时崩。
- 不支持响应式监听——虽然有OnSharedPreferenceChangeListener,但用起来极其别扭,容易内存泄漏。
- apply()的异步不可控——你以为写完了,其实还在排队,进程被杀就丢数据。
嗯,这里要注意:SharedPreferences在Android 12以上已经被标记为@Deprecated了。官方态度很明确——用DataStore。
13.2 DataStore的两种口味
DataStore分两种:Preferences DataStore 和 Proto DataStore。
| 特性 | Preferences DataStore | Proto DataStore |
|---|---|---|
| 数据类型 | 仅支持Int、Long、Float、Double、Boolean、String、Set<String> | 支持自定义对象(通过Protocol Buffers定义) |
| 类型安全 | 弱(运行时类型检查) | 强(编译时类型检查) |
| 依赖 | 仅需DataStore核心库 | 需要Protobuf编译插件 |
| 适用场景 | 简单键值对(如用户设置、开关状态) | 复杂结构化数据(如用户配置、缓存模型) |
我个人习惯:如果只是存几个开关、token、用户名,用Preferences DataStore就够了。如果涉及嵌套对象、枚举、列表,果断上Proto DataStore。
13.3 Preferences DataStore实战
先加依赖:
// build.gradle.kts (Module)
implementation("androidx.datastore:datastore-preferences:1.1.1")
然后创建一个DataStore实例。我一般把它定义在顶层扩展属性里:
val Context.settingsDataStore by preferencesDataStore(name = "settings")
读写操作全部通过Flow完成。举个例子,存一个用户是否登录的标志:
// 定义key
private val IS_LOGGED_IN = booleanPreferencesKey("is_logged_in")
// 读取——返回Flow
val Context.isLoggedInFlow: Flow<Boolean>
get() = settingsDataStore.data.map { preferences ->
preferences[IS_LOGGED_IN] ?: false
}
// 写入——挂起函数
suspend fun Context.setLoggedIn(loggedIn: Boolean) {
settingsDataStore.edit { preferences ->
preferences[IS_LOGGED_IN] = loggedIn
}
}
你看,读取返回的是Flow,天然支持响应式。UI层只要collect这个Flow,数据一变,界面自动更新。SharedPreferences能做到吗?做不到。
小技巧:如果你不想每次collect都重新发射数据,可以用.distinctUntilChanged()过滤重复值。我在项目中经常这么干,避免不必要的UI刷新。
13.4 Proto DataStore:类型安全的终极方案
Preferences DataStore虽然简单,但有个问题——key是字符串,value的类型你得自己记。万一写错了key或者类型,运行时才暴露。
Proto DataStore用Protocol Buffers定义schema,编译时生成Java/Kotlin类,类型安全拉满。
首先,定义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_logged_in = 1;
string username = 2;
int32 theme_mode = 3; // 0: light, 1: dark, 2: system
}
然后配置Protobuf编译插件(略,官方文档有详细步骤)。编译后会生成UserSettings类。
创建DataStore:
val Context.userSettingsDataStore by dataStore(
fileName = "user_settings.pb",
serializer = UserSettingsSerializer
)
Serializer需要自己实现:
object UserSettingsSerializer : Serializer<UserSettings> {
override val defaultValue: UserSettings
get() = UserSettings.getDefaultInstance()
override suspend fun readFrom(input: InputStream): UserSettings {
return UserSettings.parseFrom(input)
}
override suspend fun writeTo(t: UserSettings, output: OutputStream) {
t.writeTo(output)
}
}
读写操作:
// 读取
val Context.userSettingsFlow: Flow<UserSettings>
get() = userSettingsDataStore.data
// 写入
suspend fun Context.updateUsername(newName: String) {
userSettingsDataStore.updateData { currentSettings ->
currentSettings.toBuilder()
.setUsername(newName)
.build()
}
}
注意updateData是事务性的——如果写入过程中发生异常,整个操作会回滚,不会出现半写状态。这一点比SharedPreferences的apply()靠谱太多了。
我曾经踩过的坑:Proto DataStore的defaultValue一定要给合理值。如果你给了一个空对象,而UI层直接读取嵌套字段,可能拿到默认值0或空字符串,导致逻辑判断出错。建议在defaultValue里设置合理的业务默认值。
13.5 核心知识体系
下面这张图总结了DataStore与Flow结合的核心脉络:
13.6 在ViewModel中优雅使用
有了Flow,ViewModel里的代码可以写得很干净。我一般这样组织:
class SettingsViewModel(
private val context: Context
) : ViewModel() {
val isLoggedIn: StateFlow<Boolean> = context.isLoggedInFlow
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = false
)
fun toggleLogin() {
viewModelScope.launch {
val current = context.isLoggedInFlow.first()
context.setLoggedIn(!current)
}
}
}
这里用了stateIn把冷Flow转成热StateFlow,UI层直接观察。注意WhileSubscribed(5000)——当界面不可见时,5秒内不取消订阅,避免频繁重建。
个人经验:如果你在多个ViewModel里都要读同一个DataStore,建议封装成一个Repository,统一管理DataStore实例。不要在每个ViewModel里都创建一遍,容易造成资源浪费。
13.7 迁移指南:从SharedPreferences到DataStore
如果你想把老项目里的SharedPreferences迁移过来,Google官方提供了一个迁移工具:
val Context.dataStore by preferencesDataStore(
name = "settings",
produceMigrations = { context ->
listOf(SharedPreferencesMigration(context, "old_prefs_name"))
}
)
它会自动把SharedPreferences里的数据读到DataStore里,然后删除旧文件。整个过程对用户无感。
不过要注意:迁移是一次性的。如果旧SharedPreferences里有大量数据,第一次启动时可能会稍微慢一点。我建议在应用启动时做个异步预迁移,别让用户等。
警告:迁移过程中不要同时读写SharedPreferences和DataStore,否则可能出现数据不一致。迁移完成后,旧文件会被标记为删除,但不会立即物理删除,所以不要依赖旧文件是否存在来判断迁移状态。
13.8 总结
DataStore + Flow的组合,说白了就是Android本地存储的现代答案。它解决了SharedPreferences几乎所有痛点:主线程阻塞、类型不安全、无响应式、异步不可控。
我个人建议:新项目直接上Proto DataStore,老项目逐步迁移。别犹豫,越早换越省心。
嗯,这一章就到这里。记住:数据存储是应用的基石,选对方案,后面少踩很多坑。
公众号:蓝海资料掘金营,微信deep3321