本文是 Kotlin 项目中 Koin 依赖注入与 Android ViewModel 装配的经验总结,核心矛盾在于:容器按类型在调用时刻解析对象,而 ViewModel 的生命周期由平台掌控,两者不一致时,问题都不会在编译期暴露。
BaseViewModel 作为服务定位器 示例中的抽象基类 BaseViewModel 实现 KoinComponent,并通过 by inject<MediaPlayerHandler>() 注入共享依赖。这样写法简洁,子类不必在大量构造函数中传递依赖;代价是依赖从构造函数签名中消失:读者无法从签名看出子类使用了哪些依赖,测试必须启动容器而不能直接传入假对象,基类中每增加一个 inject() 都会影响所有 ViewModel。作者建议:所有子类都真正使用的依赖放进基类,只有部分子类需要的依赖仍保留为构造函数参数,并定期复查基类的注入清单。
注解式定义在被生成的模块加载之前不会注册任何内容 使用 @KoinViewModel、@Single、@Factory 配合 KSP 插件时,注解本身不注册任何内容,只生成模块。如果生成的模块没有被传给容器,代码仍能编译、符号处理器仍会运行,但运行时不存在任何定义。失败是部分且静默的:同时有手写定义的注解类可以正常工作,没有手写定义的类在首次解析时失败,而通过平台自身工厂获得的类完全不会察觉。作者建议只选一种定义风格并强制执行,避免手写定义与注解定义同时存在、彼此不一致。
存储所有者决定身份,而不取决于你输入的访问器 容器并不持有实例;每个访问器都会把 ViewModelStore 加 key 交给平台解析,只提供未命中时用于构建对象的工厂。没有限定符和作用域时 key 为 null,查找落到该 store 的默认槽位。by viewModels<V>() 与 by viewModel<V>() 都指向当前屏幕自己的 store:前者由平台工厂构建,后者由容器定义构建;by activityViewModels<V>() 与 by activityViewModel<V>() 都指向宿主 store,构建来源与前者分别对应。同一行内的复数/单数对会落到同一槽位并返回同一对象,后运行者由先运行者决定;跨行的差异(viewModel 与 activityViewModel)则会指向不同 store,产生两个实例。若给定义添加限定符或容器作用域,key 不再为 null,同一个所有者下就会出现两个槽位。文中建议按 import 审计而非按调用审计,并给出对应 grep 命令;如果两者都为空,说明是纯 Compose 代码库,解析点为 koinViewModel(),所有者由导航条目提供。
注册为 single 的 ViewModel 由你管理 single 适合真正会话级的 ViewModel,例如持有播放或登录状态,需要跨屏幕存活,且被非屏幕组件(widget、后台任务、通知处理器)使用;它同时意味着没有任何东西会清理它,onCleared 不会在合适时机运行。此时不应卸载定义它的模块:卸载会移除模块定义,导致屏幕外消费者在下一次解析时抛出“找不到定义”,症状往往在几分钟后的 widget 刷新中才出现。若必须重载模块以重置状态,应在创建时重载,而不要在销毁时卸载,并用 grep 查找所有 unloadKoinModules 调用,逐一确认还有谁依赖该模块、它们当前是否存活。
其他陷阱 只为从共享 flow 取一个值,应注入 repository,而不是获得一个 ViewModel;叶子 composable 在多个屏幕会各自得到独立 ViewModel,而 widget 或定时任务在组合之外没有所有者,会直接解析失败。构造函数注入的类再使用 by inject() 是混用两种方式,读者无法区分依赖来源,每个类应只选一种。按具体类注册的定义无法按接口解析,应写成 single<Interface> { … }。不要把平台宿主注册进容器来让 ViewModel 可达,容器会在重建后仍持有旧引用。
验证方法 文中给出几组命令:在基类目录 grep by inject 并统计继承者数量;用带锚点的 grep 检查注解路由与 org.koin.ksp.generated 或 defaultModule 出现情况,如果后者为空而前者不为空,说明注解未被使用;在 ViewModelModule 中检查 SharedViewModel 是否在 single 下、AlbumViewModel 是否在 viewModel 下。
适用场景:使用 Koin(含 KSP 注解)与 Android ViewModel 的 Kotlin 应用,尤其是 Compose 与多屏幕、widget 等非屏幕组件共存的工程。限制:本文是代码审查与排查经验,未提供工具安装、运行命令参数或收费信息,所有判定都以文中示例代码为前提。