9、DataStore KTX:使用DataStore替代SharedPreferences,KTX风格的读写操作
说实话,SharedPreferences 陪伴我们 Android 开发者很多年了。但每次用它,我心里都有点不踏实。同步读写可能卡 UI,apply 异步又不知道什么时候写完,多进程更是噩梦。Google 自己也明白,所以推出了 DataStore 来替代它。
今天我们就聊聊 DataStore 的 KTX 写法。说白了,就是用 Kotlin 的协程和 Flow,让数据存储变得优雅又安全。
为什么我建议你换掉 SharedPreferences
先说说我在项目里踩过的坑。有一次线上反馈说用户设置的主题色经常丢失,查了半天,原来是多线程同时读写 SharedPreferences 导致的。你想想看,一个简单的配置项,居然能搞出数据不一致,这谁受得了?
DataStore 解决了两个核心问题:
- 异步安全:所有操作都在协程里跑,不会阻塞主线程
- 类型安全:用 Kotlin 的序列化,不用再手写 key-value 字符串
而且 DataStore 有两种口味:
| 类型 | 适用场景 | 特点 |
|---|---|---|
| Preferences DataStore | 简单键值对(类似 SP) | 轻量,无需定义 schema |
| Proto DataStore | 结构化数据 | 强类型,需要定义 protobuf |
我个人习惯,如果只是存几个配置项,用 Preferences DataStore 就够了。要是数据复杂,比如用户信息、缓存列表,那就上 Proto DataStore。
Preferences DataStore 的 KTX 写法
先加依赖:
implementation("androidx.datastore:datastore-preferences:1.0.0")
然后定义一个顶层属性,用委托创建 DataStore 实例:
val Context.settingsDataStore by preferencesDataStore(
name = "settings"
)
读写操作呢?看代码:
// 写数据 - 用 edit 块
suspend fun saveUserName(context: Context, name: String) {
context.settingsDataStore.edit { preferences ->
preferences[stringPreferencesKey("user_name")] = name
}
}
// 读数据 - 返回 Flow
val userNameFlow: Flow<String> = context.settingsDataStore.data
.map { preferences ->
preferences[stringPreferencesKey("user_name")] ?: ""
}
嗯,这里要注意:edit 是挂起函数,必须在协程里调用。而 data 返回的是 Flow,天然支持响应式观察。
核心区别:SharedPreferences 是「拉」数据,DataStore 是「推」数据。数据一变,Flow 自动发射新值。
Proto DataStore 的 KTX 写法
如果数据有结构,比如用户信息包含姓名、年龄、头像,用 Preferences 就得写一堆 key,太累了。这时候 Proto DataStore 就派上用场了。
首先定义 protobuf 文件:
syntax = "proto3";
option java_package = "com.example.datastore";
option java_multiple_files = true;
message UserPreferences {
string user_name = 1;
int32 age = 2;
string avatar_url = 3;
}
然后生成代码,创建 DataStore:
val Context.userPreferencesDataStore by dataStore(
fileName = "user_preferences.pb",
serializer = UserPreferencesSerializer
)
读写操作:
// 写数据
suspend fun updateUser(context: Context, name: String, age: Int) {
context.userPreferencesDataStore.updateData { currentPreferences ->
currentPreferences.toBuilder()
.setUserName(name)
.setAge(age)
.build()
}
}
// 读数据
val userFlow: Flow<UserPreferences> = context.userPreferencesDataStore.data
你看,读出来直接就是强类型的对象,不用再手动解析字符串了。我曾经在一个老项目里,用 SharedPreferences 存 JSON 字符串,每次读都要 Gson 解析,写还要序列化,烦得很。换成 Proto DataStore 后,代码干净多了。
避坑指南:我曾经踩过的坑
讲几个我实际遇到的问题,你遇到了可以少走弯路。
坑一:DataStore 的异常处理
DataStore 的 data Flow 可能会抛出异常,比如文件损坏。建议用 catch 处理:
context.settingsDataStore.data
.catch { exception ->
if (exception is IOException) {
emit(emptyPreferences())
} else {
throw exception
}
}
.map { ... }
坑二:不要在非协程环境调用 edit
我刚开始用的时候,在 RecyclerView 的点击事件里直接调了 edit,结果编译报错。后来才意识到,必须用 lifecycleScope.launch 或者 viewModelScope.launch 包一层。
坑三:DataStore 是单例的
同一个文件名的 DataStore 在整个进程里只有一个实例。如果你在多个地方创建同名 DataStore,会抛出异常。所以建议用顶层委托,全局共享。
知识体系总览
下面这张图帮你理清 DataStore 的核心脉络:
实战:封装一个优雅的 DataStore 工具类
最后,分享一个我项目里用的封装思路。把 key 定义和读写方法集中管理,用起来特别顺手:
object SettingsKeys {
val DARK_MODE = booleanPreferencesKey("dark_mode")
val LANGUAGE = stringPreferencesKey("language")
val FONT_SIZE = intPreferencesKey("font_size")
}
class SettingsRepository(private val context: Context) {
val darkMode: Flow<Boolean> = context.settingsDataStore.data
.map { it[SettingsKeys.DARK_MODE] ?: false }
val language: Flow<String> = context.settingsDataStore.data
.map { it[SettingsKeys.LANGUAGE] ?: "zh" }
suspend fun setDarkMode(enabled: Boolean) {
context.settingsDataStore.edit {
it[SettingsKeys.DARK_MODE] = enabled
}
}
suspend fun setLanguage(lang: String) {
context.settingsDataStore.edit {
it[SettingsKeys.LANGUAGE] = lang
}
}
}
这样在 ViewModel 里调用就非常清爽了:
class SettingsViewModel(application: Application) : AndroidViewModel(application) {
private val repository = SettingsRepository(application)
val darkMode = repository.darkMode.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5000),
false
)
fun toggleDarkMode() {
viewModelScope.launch {
val current = repository.darkMode.first()
repository.setDarkMode(!current)
}
}
}
你看,整个流程下来,没有 SharedPreferences 的同步阻塞,没有 apply 的不可控,也没有 key 拼写错误的风险。DataStore + KTX 的组合,说白了就是让数据存储回归简单。
我个人觉得,这是 Android 官方这几年做得最靠谱的 API 改进之一。如果你还在用 SharedPreferences,真的可以试试换过来了。