15、DataStore:DataStore与SharedPreferences对比、Preferences DataStore、Proto DataStore
说实话,Android 里做本地轻量数据存储,大家第一反应肯定是 SharedPreferences。我早年做项目时也一直用它,直到有一次在线上遇到了 ANR 问题,追查下来发现是 SP 在主线程读写导致的。嗯,从那时候起,我就开始认真研究 DataStore 了。
这一节,咱们就把 DataStore 彻底聊透。我会从它和 SharedPreferences 的对比讲起,然后分别介绍 Preferences DataStore 和 Proto DataStore 的用法。你跟着走一遍,以后项目里该用哪个,心里就有数了。
DataStore 与 SharedPreferences 的对比
先看一张对比表,这样最直观:
| 对比维度 | SharedPreferences | DataStore |
|---|---|---|
| 线程安全 | 不保证,容易 ANR | 基于 Flow,异步安全 |
| 数据格式 | 仅支持基本类型 + String | Preferences(键值对)或 Proto(强类型) |
| 类型安全 | 无,需要手动转型 | Proto DataStore 完全类型安全 |
| 错误处理 | 无,读写异常直接崩溃 | 抛出 IOException,可捕获处理 |
| 数据迁移 | 需要手动写迁移逻辑 | 内置迁移支持 |
| 依赖注入 | 通常用静态方法获取 | 天然支持 DI(如 Hilt) |
我个人习惯是:新项目一律用 DataStore,老项目如果只是简单存几个 flag,SP 还能凑合用。但如果你要存用户配置、主题偏好、登录状态这类频繁读写的数据,我建议你直接上 DataStore。
Preferences DataStore
Preferences DataStore 是最简单的入门方式。它跟 SP 一样是键值对,但底层用 Flow 实现,天然异步。
添加依赖
// build.gradle (app)
implementation "androidx.datastore:datastore-preferences:1.0.0"
创建 DataStore
我一般会在顶层扩展属性里创建,这样全局只有一个实例:
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
写入数据
suspend fun saveThemeMode(context: Context, isDark: Boolean) {
context.dataStore.edit { preferences ->
preferences[PreferencesKeys.THEME_MODE] = isDark
}
}
object PreferencesKeys {
val THEME_MODE = booleanPreferencesKey("theme_mode")
val USER_NAME = stringPreferencesKey("user_name")
val LOGIN_COUNT = intPreferencesKey("login_count")
}
你看,写起来跟 SP 很像,但 edit 是在协程里执行的,不会卡主线程。
读取数据
val themeModeFlow: Flow<Boolean> = context.dataStore.data
.map { preferences ->
preferences[PreferencesKeys.THEME_MODE] ?: false
}
这里返回的是 Flow,你可以用 collect 或者 asLiveData 来观察变化。数据一变,UI 自动更新。
dataStore.data.first()。但大多数场景下,用 Flow 做响应式更新更优雅。
Proto DataStore
Preferences DataStore 虽然简单,但有个问题:键值对没有类型约束。你存的时候是 String,取的时候可能忘了转,就出 bug 了。Proto DataStore 就是来解决这个问题的。
它基于 Protocol Buffers(protobuf),你需要先定义一个 schema 文件,然后生成对应的 Java/Kotlin 类。说白了,就是给数据加了个「合同」,类型不对编译期就报错。
定义 proto 文件
在 app/src/main/proto/ 目录下创建 user_prefs.proto:
syntax = "proto3";
option java_package = "com.example.app";
option java_multiple_files = true;
message UserPreferences {
bool is_dark_mode = 1;
string user_name = 2;
int32 login_count = 3;
}
添加依赖和插件
// 根 build.gradle
plugins {
id "com.google.protobuf" version "0.9.1" apply false
}
// app build.gradle
plugins {
id "com.google.protobuf"
}
dependencies {
implementation "androidx.datastore:datastore:1.0.0"
implementation "com.google.protobuf:protobuf-javalite:3.21.7"
}
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:3.21.7"
}
generateProtoTasks {
all().each { task ->
task.builtins {
java {
option 'lite'
}
}
}
}
}
创建 Proto DataStore
val Context.userPrefsDataStore: DataStore<UserPreferences> by dataStore(
fileName = "user_prefs.pb",
serializer = UserPreferencesSerializer
)
object UserPreferencesSerializer : Serializer<UserPreferences> {
override val defaultValue: UserPreferences
get() = UserPreferences.getDefaultInstance()
override suspend fun readFrom(input: InputStream): UserPreferences {
return UserPreferences.parseFrom(input)
}
override suspend fun writeTo(t: UserPreferences, output: OutputStream) {
t.writeTo(output)
}
}
读写操作
// 写入
suspend fun updateDarkMode(context: Context, isDark: Boolean) {
context.userPrefsDataStore.updateData { currentPrefs ->
currentPrefs.toBuilder()
.setIsDarkMode(isDark)
.build()
}
}
// 读取
val darkModeFlow: Flow<Boolean> = context.userPrefsDataStore.data
.map { prefs -> prefs.isDarkMode }
看到没?这里读写都是通过生成的 UserPreferences 类来操作的,字段名、类型都是确定的。你不可能写错 key,也不可能取出来类型不对。
知识体系总览
下面这张图帮你理清 DataStore 的整体脉络:
避坑指南
最后分享几个我踩过的坑:
- 不要在主线程 collect:DataStore 的 data Flow 是在 IO 线程发射的,如果你在 Activity 里直接 collect 并更新 UI,记得用
lifecycleScope.launch配合Dispatchers.Main。 - 文件损坏处理:DataStore 在读写时如果遇到文件损坏,会抛出
IOException。我建议在 collect 时用 try-catch 包一下,或者用catch操作符给个默认值。 - 迁移成本:从 SP 迁移到 DataStore 可以用
SharedPreferencesMigration,但要注意 key 的映射关系。我曾经因为 key 名写错,导致用户数据全部丢失,后来加了单元测试才放心。