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。

迁移策略

  1. 双写阶段:新代码写 DataStore,同时写 SP。读的时候优先读 DataStore,如果 DataStore 没有数据,回退到 SP。
  2. 数据同步:在某个版本里,启动时把 SP 的数据一次性迁移到 DataStore。
  3. 清理阶段:确认所有用户都升级到新版本后,删除 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.IODispatchers.Main(挂起不会阻塞)。

25.7 本章小结

DataStore 配合 Koin,用起来真的很舒服。你不需要写一堆模板代码,只需要定义好 Module,然后注入到需要的地方。迁移 SP 的时候,记住「双写 + 分批 + 清理」三步走,基本不会出大问题。

我个人觉得,DataStore 最大的价值不是性能提升(虽然确实有),而是它让数据存储变得「可观察」。你想想看,以前用 SP,数据变了 UI 不知道,还得手动刷新。现在用 Flow,数据一变,UI 自动更新,这才是现代 Android 开发该有的样子。

一句话总结:Koin 让 DataStore 的注入变得简单,DataStore 让数据存储变得响应式。两者结合,你的代码会更干净、更健壮。

Koin + DataStore 迁移流程 Koin Module single { DataStore } Repository 注入 DataStore ViewModel Flow 驱动 UI SharedPreferences 迁移三步走 ① 双写阶段 同时写 SP + DataStore ② 数据同步 启动时一次性迁移 ③ 清理阶段 删除 SP 代码 最终目标:所有数据读写走 DataStore,SP 彻底退役
公众号:蓝海资料掘金营,微信 deep3321