DataStore存储:告别SharedPreferences,拥抱新时代
说实话,在Android开发中做本地轻量数据存储,大家第一反应肯定是SharedPreferences。但如果你经历过SP的坑——比如ANR、数据丢失、类型不安全——你就会明白为什么Google要推出DataStore。今天我就带你彻底搞懂DataStore,从对比到实战,一步到位。
核心结论:DataStore是SharedPreferences的官方替代品,基于协程和Flow,天生异步,类型安全,且支持两种数据格式。
DataStore vs SharedPreferences:一场降维打击
先别急着学DataStore怎么用,我们得先知道它好在哪里。我当年维护过一个老项目,SharedPreferences里存了上百个key,每次读取都提心吊胆——万一主线程读SP卡了,用户直接看到白屏。这就是SP最大的痛点:同步读写阻塞主线程。
| 对比维度 | SharedPreferences | DataStore |
|---|---|---|
| 读写方式 | 同步(可能ANR) | 异步(协程+Flow) |
| 类型安全 | 不保证(存String取Int会崩溃) | 完全类型安全 |
| 数据一致性 | 写入可能丢失 | 事务性写入,保证一致 |
| 异常处理 | 无 | Flow自带异常处理 |
| 支持数据格式 | 简单键值对 | 键值对 + 类型化对象 |
你看,SP几乎每一项都处于劣势。尤其是数据一致性——我曾经遇到过用户反馈「设置明明保存了,重启后却丢了」,查了半天发现是SP在写入过程中进程被杀。DataStore用事务性写入彻底解决了这个问题。
我的建议:新项目直接放弃SharedPreferences,全部用DataStore。老项目迁移也不难,后面我会讲迁移方案。
Preferences DataStore vs Proto DataStore
DataStore有两种实现方式,很多人一开始会混淆。简单来说:
- Preferences DataStore:类似SP的键值对,适合存简单配置(开关、字符串、数字)
- Proto DataStore:基于Protocol Buffers,需要定义schema,适合存结构化数据
怎么选?我个人的经验是:如果你的数据不超过5个key,且都是基本类型,用Preferences就够了。但如果你要存用户信息、复杂配置对象,Proto DataStore更合适——它自带类型约束,不会出现「取出来发现类型不对」的尴尬。
来看一个Preferences DataStore的典型用法:
// 定义扩展属性
val Context.settingsDataStore by preferencesDataStore(name = "settings")
// 读取
val isDarkMode: Flow<Boolean> = context.settingsDataStore.data
.map { preferences ->
preferences[PreferencesKeys.IS_DARK_MODE] ?: false
}
// 写入
suspend fun setDarkMode(enabled: Boolean) {
context.settingsDataStore.edit { preferences ->
preferences[PreferencesKeys.IS_DARK_MODE] = enabled
}
}
而Proto DataStore需要先定义proto文件:
// user_preferences.proto
syntax = "proto3";
message UserPreferences {
bool is_dark_mode = 1;
string user_name = 2;
int32 font_size = 3;
}
然后生成代码,再创建DataStore:
val Context.userPreferencesDataStore by protoDataStore(
fileName = "user_preferences.pb",
serializer = UserPreferencesSerializer
)
// 读取
val userPreferences: Flow<UserPreferences> = context.userPreferencesDataStore.data
// 写入
suspend fun updateUserName(name: String) {
context.userPreferencesDataStore.updateData { preferences ->
preferences.toBuilder()
.setUserName(name)
.build()
}
}
注意:Proto DataStore需要引入protobuf插件和依赖,构建配置稍复杂。但一旦配好,用起来非常爽——你再也不用担心key拼写错误了。
异步读写与异常处理
DataStore的所有操作都是异步的,这意味着你必须运行在协程作用域内。很多人刚接触时会问:「那我怎么在ViewModel里用?」很简单,用viewModelScope或者lifecycleScope。
但异步也带来了异常处理的问题。DataStore的data是一个Flow,它可能因为I/O错误、序列化失败等原因抛出异常。如果不处理,整个Flow会终止。
我推荐的做法是使用catch操作符:
val isDarkMode: Flow<Boolean> = context.settingsDataStore.data
.catch { exception ->
// 记录日志,返回默认值
Log.e("DataStore", "读取失败", exception)
emit(emptyPreferences())
}
.map { preferences ->
preferences[PreferencesKeys.IS_DARK_MODE] ?: false
}
写入操作也可能失败,比如磁盘空间不足。这时edit或updateData会抛出异常。我的习惯是在Repository层统一捕获:
suspend fun setDarkMode(enabled: Boolean): Result<Unit> {
return try {
context.settingsDataStore.edit { preferences ->
preferences[PreferencesKeys.IS_DARK_MODE] = enabled
}
Result.success(Unit)
} catch (e: Exception) {
Log.e("DataStore", "写入失败", e)
Result.failure(e)
}
}
避坑指南:我曾经在写入频繁的场景下忘记加catch,结果用户反馈「设置保存不了」——其实是因为某次写入异常导致后续所有写入都失败了。所以,永远不要忽略DataStore的异常处理。
DataStore与Room的选型
这是新手最容易纠结的问题。其实很简单:
- DataStore:适合存少量、简单的配置数据(几十个key以内)
- Room:适合存大量、结构化、需要查询的数据(几百条以上)
你想想看,如果你用Room存一个「是否开启暗黑模式」的开关,是不是有点杀鸡用牛刀?Room需要定义Entity、DAO、Database,还要处理数据库升级,太复杂了。反过来,如果你要存用户的1000条历史记录,用DataStore也不合适——它不支持按条件查询,每次都要全量读取。
我个人的选型原则是:
- 数据量 < 50个key,且不需要查询 → DataStore
- 数据量 > 100条,或需要SQL查询 → Room
- 两者之间 → 看业务复杂度,简单用DataStore,复杂用Room
一个项目里可以同时用DataStore和Room。比如用DataStore存用户偏好设置,用Room存离线缓存数据。它们不冲突,反而各司其职。
知识体系总览
下面这张图帮你理清DataStore的核心脉络:
这张图把DataStore的核心知识点串起来了。你可以看到,它主要解决四个问题:替代SP、两种实现、异步异常、与Room的选型。每个分支往下都有具体的子知识点。
最后说一句:DataStore的学习曲线其实很低,只要你熟悉协程和Flow,半小时就能上手。但真正用好它,需要理解它的设计哲学——异步、类型安全、事务性。记住这三点,你就能写出健壮的本地存储代码。