25. Koin与DataStore:DataStore的Koin注入、SharedPreferences迁移
说实话,DataStore 出来也有好几年了。我最早接触它的时候,心里还犯嘀咕:Google 干嘛要再搞一套?SharedPreferences 用得好好的。直到我在一个日活百万的项目里,被 SP 的 ANR 问题折磨得够呛——嗯,那次之后,我就彻底倒向了 DataStore。
这一章,咱们就聊聊怎么用 Koin 把 DataStore 注入到项目里,以及如何把老项目里的 SharedPreferences 平滑迁移过来。说白了,就是让你既学会新姿势,又能安全落地。
25.1 为什么是 DataStore?
SharedPreferences 的问题,其实很多人都知道:
- 同步读写:你在主线程调
getString(),它可能触发磁盘 I/O,直接卡 UI。 - 没有类型安全:取出来的值你得自己强转,一不小心就 ClassCastException。
- 不支持 Flow:想监听数据变化?得自己写回调,麻烦。
DataStore 用 Flow 做数据流,天然支持响应式。而且它跑在 Dispatchers.IO 上,不会阻塞主线程。我个人习惯,新项目一律用 DataStore,老项目也逐步迁移。
核心结论:DataStore 不是花架子,它是 Google 推荐的替代方案。如果你还在用 SP,建议尽早规划迁移。
25.2 DataStore 的两种口味
DataStore 分两种:
| 类型 | 适用场景 | 存储方式 |
|---|---|---|
| Preferences DataStore | 键值对存储(类似 SP) | 按 key 存取 |
| Proto DataStore | 结构化数据(需要定义 schema) | 使用 Protocol Buffers |
我个人建议:如果只是存一些简单的配置项(比如用户是否登录、主题颜色),用 Preferences DataStore 就够了。如果数据复杂(比如用户信息、缓存列表),可以考虑 Proto DataStore,但需要引入 protobuf 编译插件,成本稍高。
25.3 用 Koin 注入 Preferences DataStore
先看一个最简单的例子。假设我们要存一个「是否首次启动」的标记。
第一步:添加依赖
// build.gradle.kts (app)
implementation("androidx.datastore:datastore-preferences:1.0.0")
implementation("io.insert-koin:koin-android:3.5.0")
第二步:定义 DataStore 的 Module
// DataStoreModule.kt
val dataStoreModule = module {
single {
createDataStore(androidContext())
}
}
private fun createDataStore(context: Context): DataStore<Preferences> {
return PreferenceDataStoreFactory.create {
context.preferencesDataStoreFile("my_settings")
}
}
这里有个细节:androidContext() 是 Koin 提供的函数,必须在 Android Module 里使用。如果你在 common 模块里定义,记得传 Context 进来。
第三步:注入到 ViewModel 或 Repository
class SettingsRepository(
private val dataStore: DataStore<Preferences>
) {
companion object {
private val KEY_IS_FIRST_LAUNCH = booleanPreferencesKey("is_first_launch")
}
val isFirstLaunch: Flow<Boolean> = dataStore.data
.map { preferences ->
preferences[KEY_IS_FIRST_LAUNCH] ?: true
}
suspend fun setFirstLaunch(isFirst: Boolean) {
dataStore.edit { preferences ->
preferences[KEY_IS_FIRST_LAUNCH] = isFirst
}
}
}
然后在 Koin 里注册 Repository:
val repositoryModule = module {
single { SettingsRepository(get()) }
}
你看,Koin 的注入方式非常自然。不需要写 Dagger 那种 @Module、@Provides 注解,直接一个 single { } 搞定。
小提示:DataStore 的 data 属性返回的是 Flow,所以你在 ViewModel 里可以直接用 stateIn 转成 StateFlow,配合 Compose 或 LiveData 都很方便。
25.4 用 Koin 注入 Proto DataStore
Proto DataStore 稍微复杂一点,但思路是一样的。你需要先定义 .proto 文件,然后生成 Java 类。这里我不展开 protobuf 的配置,只讲 Koin 注入的部分。
// ProtoDataStoreModule.kt
val protoDataStoreModule = module {
single {
createProtoDataStore(androidContext())
}
}
private fun createProtoDataStore(context: Context): DataStore<UserSettings> {
return ProtoDataStoreFactory.create(
serializer = UserSettingsSerializer(),
produceFile = { context.dataStoreFile("user_settings.pb") }
)
}
其中 UserSettingsSerializer 需要实现 Serializer<UserSettings> 接口,处理序列化和反序列化逻辑。这部分代码比较固定,网上有很多模板,我就不贴了。
25.5 从 SharedPreferences 迁移到 DataStore
这是很多团队最头疼的部分。老项目里 SP 存了一大堆 key,怎么迁?
我的建议是:不要一次性全迁,分模块、分阶段进行。我曾经在一个项目里,SP 里存了 50 多个 key,涉及登录、支付、推送等多个模块。如果一次性全改,测试成本太高,而且容易出 bug。
迁移策略
- 双写阶段:新代码写 DataStore,同时写 SP。读的时候优先读 DataStore,如果 DataStore 没有数据,回退到 SP。
- 数据同步:在某个版本里,启动时把 SP 的数据一次性迁移到 DataStore。
- 清理阶段:确认所有用户都升级到新版本后,删除 SP 的读写代码。
代码示例:双写 + 迁移
class MigrationManager(
private val context: Context,
private val dataStore: DataStore<Preferences>
) {
private val prefs = context.getSharedPreferences("old_prefs", Context.MODE_PRIVATE)
suspend fun migrateIfNeeded() {
val migrated = dataStore.data.first()[KEY_MIGRATED] ?: false
if (migrated) return
// 读取 SP 中的所有数据
val allEntries = prefs.all
dataStore.edit { preferences ->
allEntries.forEach { (key, value) ->
when (value) {
is String -> preferences[stringPreferencesKey(key)] = value
is Int -> preferences[intPreferencesKey(key)] = value
is Boolean -> preferences[booleanPreferencesKey(key)] = value
is Float -> preferences[floatPreferencesKey(key)] = value
is Long -> preferences[longPreferencesKey(key)] = value
}
}
preferences[booleanPreferencesKey("migrated")] = true
}
// 迁移完成后,可以删除 SP 文件(可选)
prefs.edit().clear().apply()
}
}
然后在 Application 启动时调用:
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidContext(this@MyApp)
modules(dataStoreModule, repositoryModule)
}
// 启动迁移(在后台线程)
CoroutineScope(Dispatchers.IO).launch {
val migrationManager = MigrationManager(this@MyApp, get())
migrationManager.migrateIfNeeded()
}
}
}
注意:迁移过程是异步的,不要在启动时阻塞主线程。另外,如果 SP 数据量很大(比如几百个 key),建议分批次迁移,避免一次写入太多数据导致 DataStore 文件损坏。
25.6 避坑指南
我踩过几个坑,分享给你:
- DataStore 文件冲突:如果你在多个进程里同时读写同一个 DataStore 文件,会抛出
IOException。解决方案是给每个进程分配独立的 DataStore 实例,或者用MultiProcessDataStore(不推荐,性能差)。 - 迁移时类型不匹配:SP 里存的 key 可能是 String,但 DataStore 里你定义成了 Int。迁移时一定要做类型检查,否则会崩溃。
- 不要在主线程调用
dataStore.edit:虽然 DataStore 内部会切到 IO 线程,但edit是挂起函数,如果你在协程里调用,确保协程上下文是Dispatchers.IO或Dispatchers.Main(挂起不会阻塞)。
25.7 本章小结
DataStore 配合 Koin,用起来真的很舒服。你不需要写一堆模板代码,只需要定义好 Module,然后注入到需要的地方。迁移 SP 的时候,记住「双写 + 分批 + 清理」三步走,基本不会出大问题。
我个人觉得,DataStore 最大的价值不是性能提升(虽然确实有),而是它让数据存储变得「可观察」。你想想看,以前用 SP,数据变了 UI 不知道,还得手动刷新。现在用 Flow,数据一变,UI 自动更新,这才是现代 Android 开发该有的样子。
一句话总结:Koin 让 DataStore 的注入变得简单,DataStore 让数据存储变得响应式。两者结合,你的代码会更干净、更健壮。