用了这么久 Koin Scope,原来一直都用错了?

你有没有遇到过,用户换了头像,首页更新了。搜索页却还是旧头像。排查了半天,最终发现是Koin的作用域使用错了。

Koin 官方博客最近有个很小但很实用的例子:在一个类似 Netflix 的 Compose Multiplatform 项目里,用户可以选择不同 profile。

这个需求看起来只是"换头像"。但真正的问题是:当前选中的 profile 图片,要在整个 App 内保持一致。首页要显示它,搜索页要显示它,后续可能还有详情页、设置页、播放页都要显示它。如果用户重新选择 profile,所有页面都应该读到新的图片。这类状态最容易出问题。

问题在哪里

很多项目处理这类需求时,会下意识把它塞进全局单例。

写起来确实快。

一个 CurrentUserHolder,一个 selectedProfileUrl,哪里要就哪里读。短期看很顺手,长期看就开始难受。它什么时候创建?什么时候清空?

用户退出登录、切换用户、切换 profile、重新进入应用时,它到底还该不该存在?这些问题如果没有答案,单例就会慢慢变成全局垃圾桶。

另一种做法是存到数据库或 DataStore。但 profile 图片这个例子里,当前选择更像"会话状态",不是长期业务数据。

它应该随着当前会话存在,也应该随着当前会话结束而消失。这就是 Koin 自定义 Scope 适合出场的位置。

Scope解决的是边界

Koin 里最常见的是 singlefactorysingle 表示全局共享一个实例。factory 表示每次注入都创建一个新实例。

但真实业务里,经常有第三种生命周期:只在某段流程或某个会话里共享。

比如当前用户会话、当前 workspace、当前 profile、一次 checkout 流程、一次编辑草稿流程。它们不应该全局永生,也不应该每次读取都变成新对象。自定义 Scope 解决的就是这个边界。

在官方博客的例子里,可以先定义一个代表用户会话的类型:

bash 复制代码
class SessionScope

再定义真正需要保存在会话里的轻量数据:

bash 复制代码
data class UserSessionInfo(
    val changedAt: Instant,
    val profileImageUrl: String
)

这里有个很重要的细节:只放必要信息。不要顺手把完整用户对象、token、偏好配置、各种缓存全塞进去。Scope 不是新的杂物间。它只是一个有明确生命周期的容器。

Manager只做入口

官方博客里最值得借鉴的设计,是 SessionManager 不保存状态。

它不是一个带成员变量的全局 Holder。它只负责三件事:打开会话、读取会话、关闭会话。

打开会话时,先关闭旧 scope,再创建新 scope,然后把 UserSessionInfo 声明进去。

读取会话时,从 Koin 当前 scope 里取。关闭会话时,把这个 scope 关掉。

可以简化理解成这样:

bash 复制代码
class SessionManager {
    private val scopeId = "user_session"

    fun openSession(imageUrl: String) {
        closeSession()

        val scope = KoinPlatform.getKoin()
            .createScope<SessionScope>(scopeId)

        scope.declare(
            UserSessionInfo(
                changedAt = Clock.System.now(),
                profileImageUrl = imageUrl
            )
        )
    }

    fun userSessionInfo(): UserSessionInfo? {
        return KoinPlatform.getKoin()
            .getScopeOrNull(scopeId)
            ?.getOrNull<UserSessionInfo>()
    }

    fun closeSession() {
        KoinPlatform.getKoin()
            .getScopeOrNull(scopeId)
            ?.close()
    }
}

这段代码的关键不在 API,而在职责分离。状态在 Scope 里。Manager 只是入口。所以它注册成 factory 也没有问题。

每次拿到新的 SessionManager 实例,读的仍然是同一个 user_session scope。

如果你把状态存在 manager 成员变量里,那 factory 就会立刻出问题。

这也是判断设计是否清晰的一个好办法:换成 factory 后,状态边界还成立吗?

什么时候该用

判断一个状态该不该放进自定义 Scope,可以问三个问题。

它是不是只在一段会话或流程里有效?它是不是需要跨多个页面、UseCase 或服务共享?它是不是有明确的开始和结束?

如果三个答案都是"是",Scope 通常值得考虑。

当前登录会话适合。当前用户或 workspace 适合。当前 profile 适合。

一次支付流程里的上下文,也可能适合。反过来,如果数据要长期保存,应该用数据库、DataStore 或服务端。如果数据只属于单个页面,ViewModel 就够了。

如果对象真的要全 App 共享,而且生命周期等于应用生命周期,single 更直接。不要为了显得高级而用 Scope。Scope 的价值,是让生命周期表达得更准确。

容易踩的坑

第一个坑,是把 Scope 当单例的替身。如果你的 scope 永远不关闭,那它只是一个换了名字的全局对象。

第二个坑,是往 Scope 里塞太多东西。Scope 里的数据越多,边界越模糊,最后就越难判断谁该负责清理。

第三个坑,是把 Manager 写成状态容器。一旦 manager 自己保存 profileImageUrl,Koin Scope 的意义就被削弱了。

第四个坑,是以为 Scope 会自动刷新 UI。

不,它不会。

你仍然需要明确设计状态流转。你项目里现在有没有一个"大家都在读,但没人知道什么时候清空"的对象?如果有,它很可能就是 Scope 的候选对象。

写在最后

如果你也在 Kotlin 或 Compose Multiplatform 项目里处理当前用户、当前 profile 这类状态,可以回头检查一下:它们现在到底活在哪里?

你更习惯用单例、ViewModel、DataStore,还是 Koin Scope 管这类状态?欢迎评论区聊聊。

#Koin #Kotlin #ComposeMultiplatform #依赖注入 #Android开发

相关推荐
爱勇宝14 小时前
我做了一个只用来搜歌词的小 App
android·前端·后端
众少成多积小致巨17 小时前
JNI (Java Native Interface) 技术手册中文参考指南
android·java·c++
唐青枫1 天前
Kotlin Context Parameters 详解:别再把 Logger、事务和配置层层往下传
kotlin
Coffeeee1 天前
如何使用Glide和Coil加载WebP动图
android·kotlin·glide
Kapaseker1 天前
5 分钟搞懂 Kotlin DSL
android·kotlin
恋猫de小郭1 天前
AI Agent 开发究竟是啥?如何用 AI 开发 Agent ?深入浅出给你一套概念
android·前端·ai编程
黄林晴1 天前
Android 17 正式发布!target 37 一大批旧代码直接不能用了
android
Carson带你学Android1 天前
Android 17 正式发布:AI 终于成了系统能力
android·前端·ai编程
三少爷的鞋1 天前
当 UseCase 开始长期监听,它可能已经不是 UseCase 了
android