26、DataStore:告别SharedPreferences,拥抱现代化存储
说实话,做Android开发这么多年,SharedPreferences 就像一位老朋友。它简单、直接,几乎每个项目都用过。但这位老朋友,其实有不少“坏毛病”。
我记得刚入行那会儿,在一个日活百万的App里,就因为SP的同步写入,导致UI线程卡顿,被用户骂惨了。后来排查才发现,是某个第三方SDK在频繁写SP。
所以,当Jetpack DataStore出现时,我几乎是第一时间就切了过去。今天,我们就来彻底聊聊它。
DataStore vs SharedPreferences:为什么换?
先看一张对比表,你就明白了。
| 对比维度 | SharedPreferences | DataStore |
|---|---|---|
| 线程安全 | 不保证(容易ANR) | 基于Flow,异步安全 |
| 数据一致性 | 写入时可能丢失 | 事务性写入,原子操作 |
| 异常处理 | 运行时异常 | 返回Result,可捕获 |
| 类型安全 | 仅支持基本类型+String | 支持自定义对象(Proto) |
| 监听变化 | OnSharedPreferenceChangeListener(易泄漏) | Flow,响应式,生命周期安全 |
说白了,SP是Java早期设计的产物,而DataStore是Kotlin协程时代的亲儿子。如果你还在用SP,我建议你认真考虑迁移。
Preferences DataStore:键值对存储的“轻骑兵”
如果你只是存一些简单的配置,比如用户是否登录、主题颜色、音量大小,那用 Preferences DataStore 就够了。它和SP的用法很像,但底层完全重写了。
添加依赖
// build.gradle (app)
implementation "androidx.datastore:datastore-preferences:1.0.0"
创建DataStore实例
我个人习惯用顶层属性来持有单例,这样全局都能用。
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
写入数据
注意,这里用的是 suspend函数,必须在协程里调用。
suspend fun saveUserName(name: String) {
context.dataStore.edit { preferences ->
preferences[stringPreferencesKey("user_name")] = name
}
}
读取数据
读取返回的是 Flow,可以实时监听变化。你想想看,以前用SP监听变化多麻烦?还要反注册。
val userNameFlow: Flow<String?> = context.dataStore.data.map { preferences ->
preferences[stringPreferencesKey("user_name")]
}
first() 操作符。但大多数场景下,用Flow更优雅。
Proto DataStore:当数据变得复杂
如果你的配置项很多,或者有嵌套结构,比如用户信息包含姓名、年龄、地址,那用Preferences DataStore就会很痛苦——你得手动拼接Key。
这时候,Proto DataStore 就派上用场了。它基于Protocol Buffers,强类型、结构清晰。
定义proto文件
在 app/src/main/proto/ 目录下创建 user.proto:
syntax = "proto3";
option java_package = "com.example.app";
option java_multiple_files = true;
message UserPreferences {
string user_name = 1;
int32 age = 2;
bool is_logged_in = 3;
}
添加protobuf插件
// 根build.gradle
plugins {
id "com.google.protobuf" version "0.9.1" apply false
}
// app/build.gradle
plugins {
id "com.google.protobuf"
}
protobuf {
protoc { artifact = "com.google.protobuf:protoc:3.21.7" }
generateProtoTasks {
all().each { task ->
task.builtins {
java { option 'lite' }
}
}
}
}
创建DataStore实例
val Context.userDataStore: DataStore<UserPreferences> by dataStore(
fileName = "user.pb",
serializer = UserPreferencesSerializer
)
自定义Serializer
嗯,这里要注意,Serializer是必须实现的。它告诉DataStore怎么序列化和反序列化。
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 updateUser(name: String, age: Int) {
context.userDataStore.updateData { current ->
current.toBuilder()
.setUserName(name)
.setAge(age)
.build()
}
}
// 读取
val userFlow: Flow<UserPreferences> = context.userDataStore.data
实战:键值对存储的完整示例
我们做一个简单的“用户设置”存储,包含主题、语言、通知开关。
1. 定义Key
object SettingsKeys {
val THEME = stringPreferencesKey("theme")
val LANGUAGE = stringPreferencesKey("language")
val NOTIFICATIONS_ENABLED = booleanPreferencesKey("notifications_enabled")
}
2. 封装Repository
class SettingsRepository(private val dataStore: DataStore<Preferences>) {
val settingsFlow: Flow<Settings> = dataStore.data.map { prefs ->
Settings(
theme = prefs[THEME] ?: "light",
language = prefs[LANGUAGE] ?: "zh",
notificationsEnabled = prefs[NOTIFICATIONS_ENABLED] ?: true
)
}
suspend fun updateTheme(theme: String) {
dataStore.edit { it[THEME] = theme }
}
suspend fun updateLanguage(language: String) {
dataStore.edit { it[LANGUAGE] = language }
}
suspend fun toggleNotifications(enabled: Boolean) {
dataStore.edit { it[NOTIFICATIONS_ENABLED] = enabled }
}
}
data class Settings(
val theme: String,
val language: String,
val notificationsEnabled: Boolean
)
3. 在ViewModel中使用
class SettingsViewModel(private val repository: SettingsRepository) : ViewModel() {
val settings: StateFlow<Settings> = repository.settingsFlow
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), Settings("light", "zh", true))
fun setTheme(theme: String) {
viewModelScope.launch {
repository.updateTheme(theme)
}
}
}
知识体系总览
下面这张图,帮你理清DataStore的整个脉络。
总结
DataStore 不是银弹,但它确实解决了SP的很多痛点。我个人建议:
- 新项目直接上DataStore,别犹豫
- 老项目如果SP用得不多,可以逐步迁移
- 如果数据量很大(比如几百个Key),考虑用Room
记住,技术选型没有绝对的对错,但选择更现代、更安全的方案,总是没错的。
公众号:蓝海资料掘金营,微信deep3321