13. Hilt与DataStore:DataStore的注入、SharedPreferences迁移方案
说实话,DataStore 出来也有好几年了。但我发现很多项目还在用 SharedPreferences,问就是「懒得改」。嗯,我能理解——毕竟 SP 用了那么多年,也没出过什么大乱子。但如果你正在用 Hilt 做依赖注入,那迁移到 DataStore 其实比你想象中要轻松得多。
我个人习惯是,新项目直接上 DataStore,老项目逐步迁移。今天我们就聊聊怎么用 Hilt 把 DataStore 注入到项目里,以及如何把 SharedPreferences 平滑地迁移过来。
13.1 为什么是 DataStore?
先简单说说 DataStore 解决了什么问题。SharedPreferences 有几个老毛病:
- 同步读写会卡 UI 线程——你想想看,主线程里调个
getString(),万一文件大一点,界面就掉帧了 - 类型不安全——存的时候是 String,取的时候可能当成 Int 用,运行时才崩
- 没有事务保证——写入一半进程被杀,数据就坏了
DataStore 基于 Flow,天生异步,还支持类型安全。说白了,它就是 Google 给 SP 准备的接班人。
13.2 用 Hilt 注入 DataStore
这部分我踩过坑。一开始我直接在 Activity 里创建 DataStore,结果每个页面都要传 Context,代码丑得不行。后来用 Hilt 做单例注入,整个世界清净了。
13.2.1 添加依赖
首先,在 build.gradle 里加上 DataStore 和 Hilt 的依赖:
// DataStore
implementation "androidx.datastore:datastore-preferences:1.0.0"
// Hilt(如果你还没加)
implementation "com.google.dagger:hilt-android:2.48"
kapt "com.google.dagger:hilt-compiler:2.48"
13.2.2 定义 Module 提供 DataStore
我习惯把 DataStore 的创建放在一个单独的 Hilt Module 里。这样整个应用只需要一个实例,不会重复创建文件。
@Module
@InstallIn(SingletonComponent::class)
object DataStoreModule {
@Provides
@Singleton
fun provideDataStore(@ApplicationContext context: Context): DataStore<Preferences> {
return context.createDataStore(
name = "app_settings"
)
}
}
这里有个细节:@ApplicationContext 是 Hilt 提供的 qualifier,用来获取应用级别的 Context。千万别传 Activity 的 Context,否则 DataStore 的生命周期会乱掉。
13.2.3 封装 Repository
直接暴露 DataStore 给 ViewModel 其实不太优雅。我一般会再包一层 Repository,把读写逻辑封装起来。
class SettingsRepository @Inject constructor(
private val dataStore: DataStore<Preferences>
) {
companion object {
private val KEY_USER_NAME = stringPreferencesKey("user_name")
private val KEY_NOTIFICATIONS_ENABLED = booleanPreferencesKey("notifications_enabled")
}
val userName: Flow<String> = dataStore.data
.map { preferences ->
preferences[KEY_USER_NAME] ?: ""
}
val notificationsEnabled: Flow<Boolean> = dataStore.data
.map { preferences ->
preferences[KEY_NOTIFICATIONS_ENABLED] ?: true
}
suspend fun saveUserName(name: String) {
dataStore.edit { preferences ->
preferences[KEY_USER_NAME] = name
}
}
suspend fun setNotificationsEnabled(enabled: Boolean) {
dataStore.edit { preferences ->
preferences[KEY_NOTIFICATIONS_ENABLED] = enabled
}
}
}
你看,这样 ViewModel 里只需要注入 SettingsRepository,完全不用关心 DataStore 的细节。测试的时候也方便 mock。
13.2.4 在 ViewModel 中使用
@HiltViewModel
class SettingsViewModel @Inject constructor(
private val settingsRepo: SettingsRepository
) : ViewModel() {
val userName: StateFlow<String> = settingsRepo.userName
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), "")
fun updateName(name: String) {
viewModelScope.launch {
settingsRepo.saveUserName(name)
}
}
}
stateIn() 把 Flow 转成 StateFlow 时,WhileSubscribed(5000) 是个好习惯。它会在订阅消失后等待 5 秒再取消上游,避免配置变更时重复创建 DataStore 的协程。
13.3 SharedPreferences 迁移方案
老项目迁移是个细致活。我曾经在一个百万 DAU 的应用里做过迁移,踩了不少坑。这里分享一套稳妥的方案。
13.3.1 迁移策略对比
| 策略 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 一次性迁移 | 代码干净,无历史包袱 | 需要改所有调用点,风险高 | 小项目或新模块 |
| 双写双读 | 可灰度,可回滚 | 维护两套代码,工作量大 | 大型项目,需要逐步切换 |
| 代理模式 | 对业务代码无侵入 | 需要设计抽象层 | 中大型项目,团队有设计能力 |
我个人推荐「代理模式」。说白了,就是定义一个接口,然后分别实现 SP 版本和 DataStore 版本。切换时只需要改 Hilt 的 Module 绑定。
13.3.2 代理模式实战
先定义一个接口:
interface SettingsStorage {
fun getUserName(): Flow<String>
suspend fun saveUserName(name: String)
fun isNotificationsEnabled(): Flow<Boolean>
suspend fun setNotificationsEnabled(enabled: Boolean)
}
然后分别实现:
// SharedPreferences 实现
class SharedPrefsSettingsStorage @Inject constructor(
@ApplicationContext private val context: Context
) : SettingsStorage {
private val prefs = context.getSharedPreferences("app_settings", Context.MODE_PRIVATE)
override fun getUserName(): Flow<String> = flow {
emit(prefs.getString("user_name", "") ?: "")
}
override suspend fun saveUserName(name: String) {
prefs.edit().putString("user_name", name).apply()
}
// ... 其他方法
}
// DataStore 实现
class DataStoreSettingsStorage @Inject constructor(
private val dataStore: DataStore<Preferences>
) : SettingsStorage {
// 代码同前面的 SettingsRepository
}
最后在 Hilt Module 里绑定:
@Module
@InstallIn(SingletonComponent::class)
abstract class StorageModule {
@Binds
@Singleton
abstract fun bindSettingsStorage(
impl: DataStoreSettingsStorage // 想切回 SP 就改成 SharedPrefsSettingsStorage
): SettingsStorage
}
这样,业务代码完全不用改。哪天 DataStore 出了更好的替代品,换个实现类就行。
13.3.3 数据迁移工具
DataStore 官方提供了一个迁移工具,可以把 SP 的数据一次性搬过来。用法很简单:
val dataStore = context.createDataStore(
name = "app_settings",
migrations = listOf(
SharedPreferencesMigration(context, "old_shared_prefs_name")
)
)
第一次创建 DataStore 时,它会自动读取旧的 SP 文件,把数据写进 DataStore,然后删除 SP 文件。嗯,这里要注意——迁移只执行一次。如果用户已经升级过版本,第二次启动就不会再跑了。
13.4 知识体系总览
下面这张图总结了本章的核心逻辑:
13.5 避坑指南
最后,分享几个我实际项目中遇到的坑:
- DataStore 的
data属性是 Flow,但第一次读取可能为空。 如果你用.first()拿值,记得给默认值。否则 UI 上可能出现空白。 - 不要在
edit块里做耗时操作。 DataStore 的写操作是单线程队列执行的,如果你在edit里做网络请求,整个 DataStore 的写入都会被卡住。 - 迁移时注意 SP 的 key 冲突。 如果旧 SP 里有个 key 叫 "user_name",新 DataStore 里也有,迁移工具会以 SP 的值为准。如果你已经在新版本里写了新数据,迁移可能会覆盖掉。
- 测试时记得用
runBlockingTest或runTest。 DataStore 的读写都是挂起函数,普通单元测试跑不了。我一般会用Turbine库来测试 Flow 的发射值。
好了,DataStore 的注入和迁移就聊到这儿。说白了,Hilt 让 DataStore 的接入变得特别简单——一个 Module、一个 Repository,剩下的就是业务代码的事了。至于迁移,别想着一步到位,用代理模式慢慢来,稳得很。