关于 koin
面向 Kotlin Android 与 KMP 开发者的 Koin 依赖注入参考技能,聚焦跨平台模块组织、Compose 与作用域等易错点,并说明依赖图验证和渐进式注解用法。
主要功能
- 01KMP source set 依赖组织
- 02KSP 注解模块生成
- 03Compose ViewModel 注入
- 04Activity 保留与自定义作用域
- 05verify() 依赖图检查
适用场景
- 01为 Android 与 iOS 配置平台依赖
- 02通过注解生成并注册模块
- 03管理配置变更期间的对象生命周期
- 04在测试中发现缺失依赖绑定
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ rcosteira79/ android-skills --skill koin安装后在 Claude Code 中输入/koin确认已加载。
详细介绍
Koin 依赖注入(Android 和 KMP)
务实的 Kotlin DI。Koin 与 Hilt 的区别: Koin 可在 commonMain 中运行(Hilt 不行),并通过 verify() 验证依赖图,而不是使用代码生成;Hilt 仅支持 Android,采用代码生成并深度集成 Jetpack(@HiltViewModel / hiltViewModel())。本文介绍容易踩坑的地方和适用边界,不涵盖 Classic DSL 基础(single / factory / scoped / 使用 get() 解析和运行时参数的 viewModel { }、koinViewModel / koinInject、startKoin { androidLogger(); androidContext(this@MyApp); modules(...) })。相关内容: android-skills:kmp-ktor、android-skills:android-data-layer。依赖通过 koin-bom 管理:koin-core、koin-android、koin-androidx-compose、koin-compose-viewmodel(KMP)、koin-compose-viewmodel-navigation(Nav 3)、koin-test,以及可选的 koin-annotations / koin-ksp-compiler。
KSP 注解 — 生成的模块访问器
@Module @ComponentScan 会发现带注解的类,并生成一个传给 startKoin 的模块,即 UserModule().module —— 带注解类上生成的 .module 扩展是最容易遗漏的部分。每个模块选用一种风格(不要在同一个模块中混用 Classic DSL 和注解;不同模块之间可以切换)。
@Single class UserRepositoryImpl(private val service: UserService) : UserRepository
@Factory class UserFormValidator
@KoinViewModel class UserDetailViewModel(
@InjectedParam private val userId: String,
private val repository: UserRepository,
) : ViewModel()
@Module @ComponentScan("com.example.feature.user") class UserModule
// startKoin { modules(UserModule().module) }
KMP 源集布局
大多数绑定放在 commonMain 中;依赖平台类型的绑定(HTTP 引擎、Context、Keychain)应通过 expect val platformModule: Module 隔离,并在每个平台提供一个 actual(与 android-skills:kmp-ktor 中的引擎模式相同)。iOS 启动:从 iOSApp.init() 调用 InitKoinKt.doInitKoin(config: nil) —— Swift 将 init 作为保留名称,因此添加了 do 前缀。
// commonMain
expect val platformModule: Module
// androidMain: actual = module { single<HttpClientEngine> { OkHttp.create() }; single<TokenStorage> { DataStoreTokenStorage(get()) } }
// iosMain: actual = module { single<HttpClientEngine> { Darwin.create() }; single<TokenStorage> { KeychainTokenStorage() } }
Compose:调用点故障
koinViewModel / koinInject 需要 koin-androidx-compose(Android)或 koin-compose-viewmodel(KMP)。缺少该依赖时,调用可以通过编译,但会在调用点失败 —— koin-core 并不了解 ViewModel 或 Compose 运行时。为便于测试,可组合函数的参数默认值使用 koinInject():fun Screen(service: AnalyticsService = koinInject())。
val vm: UserDetailViewModel = koinViewModel { parametersOf(userId) }
// keyed per entity: koinViewModel(key = "detail_$userId", parameters = { parametersOf(userId) })
作用域
跨配置更改保留(常见情况)→ activityRetainedScope —— 由 Activity 保留的 ViewModel 支撑,因此在屏幕旋转期间仍会保留,并在 Activity 真正结束时关闭。使用 activityRetainedScope { }(或 @ActivityRetainedScope)声明,并通过 AndroidScopeComponent 访问;这种情况下不要手动实现 createScope / close。
val checkoutModule = module {
activityRetainedScope { // retained across config changes
scoped { CheckoutCart() }
scoped { CheckoutPricing(get()) }
}
}
class CheckoutActivity : AppCompatActivity(), AndroidScopeComponent {
override val scope: Scope by activityRetainedScope()
private val cart: CheckoutCart by inject()
}
不与 Activity 绑定的自定义生命周期(例如以订单 ID 为键的多步骤流程)→ 声明一个 scope<T> 并管理其生命周期:getKoin().createScope<CheckoutFlow>("checkout-$orderId"),使用 scope.get() 解析,并在流程结束时调用 scope.close()。
Nav 3
koinEntryProvider() 会按目标位置解析 ViewModel;应在模块中注册目标位置,而不是直接写在 NavDisplay 位置。
val navigationModule = module {
navigation<HomeRoute> { HomeScreen(viewModel = koinViewModel()) }
navigation<DetailRoute> { route -> DetailScreen(viewModel = koinViewModel { parametersOf(route.id) }) }
}
NavDisplay(backStack = backStack, onBack = { backStack.removeLastOrNull() }, entryProvider = koinEntryProvider())
测试 — verify() / checkModules()
遍历每个声明的构造函数,并确认所有依赖都已声明 —— 缺失的绑定会成为测试失败,而不是运行时的 NoDefinitionFoundException(弥补 Hilt 的一项优势)。在 CI 中运行。对于运行时解析的构造函数参数(例如 SavedStateHandle),请传入 extraTypes,否则 verify() 会误报。使用伪实现替换真实绑定(例如 Ktor MockEngine —— 参见 android-skills:kmp-ktor)。KoinTestRule(JUnit 4)/ KoinTestExtension(JUnit 5)会为每个测试安装上下文。
class ModuleVerificationTest : KoinTest {
@Test fun `all modules resolve cleanly`() {
koinApplication { modules(appModule, networkModule, platformModule) }.checkModules()
}
// Per-module with runtime-resolved types:
// featureModule.verify(extraTypes = listOf(SavedStateHandle::class))
}