Wnb koin feature module
Skill wenubey/claude-android-skills/skills/wnb-koin-feature-module
Reusable Claude Code skills for Android — Kotlin, Jetpack Compose, unit + UI testing, animations
npx -y skills add wenubey/claude-android-skills --skill wnb-koin-feature-moduleAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- 15 days oldThe repository was created 15 days ago. New is not bad, but a brand new repository carrying a familiar-sounding name is the shape a typosquat arrives in, and there has been no time for anyone else to find a problem with it.
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Use this skill when adding a new feature to an Android + Koin project, or when reviewing dependency injection wiring. Enforces feature-scoped Koin modules — one `Module` per feature package (customerModule, sellerModule, authModule, …) that bundles the feature's ViewModels and feature-only bindings, alongside concern-scoped modules (databaseModule, ktorModule, dispatcherModule, firebaseModule) for cross-cutting infrastructure. Requires `viewModelOf(::XxxViewModel)` for simple constructors, `viewModel { … }` for manual wiring, `single` vs `factory` semantics, `named(...)` qualifier for parallel bindings of the same type, all modules merged into a single `appModules` list, `startKoin { modules(appModules) }` only in `Application.onCreate`. Triggers on "add koin module", "koin binding", "viewModelOf", "koin module", "single vs factory", "named qualifier", "startKoin", "loadKoinModules", "feature module", "DI wiring", "inject viewmodel".
SKILL.md
8.3 KB, as published. Nobody here has run it
Koin — feature-scoped module structure
This skill covers how to organize Koin modules in a growing Android codebase. The core rule: as an app scales, a single monolithic viewModelModule listing every ViewModel becomes a merge-conflict magnet and a review-friction generator. Split by feature; keep infrastructure by concern.
Pairs with [[wnb-viewmodel-udf]] — the VM shape the module binds.
Non-negotiables
- Feature-scoped modules for feature code. One
Moduleper feature package (customerModule,sellerModule,authModule,adminModule, …). Each module bundles that feature's ViewModels and any bindings only that feature uses. - Concern-scoped modules for cross-cutting infrastructure.
databaseModule,ktorModule/firebaseModule,dispatcherModule,preferencesModule,connectivityModule,workerModule. These stay concern-scoped because they are consumed by every feature. viewModelOf(::XxxViewModel)when the constructor is Koin-injectable end-to-end. Only fall back toviewModel { XxxViewModel(get(), get(named("foo")), get()) }when you need qualifiers,SavedStateHandle, or manual argument massaging.singlevsfactoryvsviewModel:single { }— one instance per Koin scope. Use for repositories, DAOs, HTTP clients, dispatcher providers.factory { }— new instance everyget(). Use for lightweight helpers you don't want to leak state across.viewModel { }/viewModelOf(...)— one instance perViewModelStoreOwner. Neversinglea ViewModel.
named("...")qualifier when two bindings share a type. Example: twoDataStore<Preferences>instances (named("pendingSync"),named("session")). Consumers must use the same qualifier atget(named("...")).- All modules merged into a single
appModules: List<Module>. One import point. startKoin { modules(appModules) }only inApplication.onCreate(). Never anywhere else. Tests useloadKoinModules/unloadKoinModulesinside aKoinTestRule, notstartKoin.- No circular module dependencies. Feature modules depend on infrastructure modules; infrastructure never depends on features. If a "cross-feature" binding is needed, promote it to a concern module.
Canonical shape
// di/CustomerModule.kt — feature-scoped
val customerModule = module {
// ViewModels for this feature
viewModelOf(::CustomerHomeViewModel)
viewModelOf(::CustomerProductDetailViewModel)
viewModelOf(::CartViewModel)
viewModelOf(::WishlistViewModel)
viewModel {
CheckoutViewModel(
paymentRepository = get(),
cartRepository = get(),
addressRepository = get(),
authRepository = get(),
connectivityObserver = get(),
discountRepository = get(),
dispatcherProvider = get(),
)
}
// Feature-only helper — not used outside customer package
single { CustomerPricingFormatter(get()) }
}
// di/DataModule.kt — concern-scoped: repositories are consumed by every feature
val repositoryModule = module {
single<AuthRepository> { AuthRepositoryImpl(get(), get(), get()) }
single<CartRepository> { CartRepositoryImpl(get(), get()) }
single<AddressRepository> { AddressRepositoryImpl(get(), get()) }
single<PaymentRepository> { PaymentRepositoryImpl(get(), get(), get()) }
single<DiscountRepository> { DiscountRepositoryImpl(get(), get()) }
}
// di/DispatcherModule.kt — the injectable IO/Main/Default abstraction
val dispatcherModule = module {
single<DispatcherProvider> { DefaultDispatcherProvider() }
}
// di/PreferencesModule.kt — named qualifiers for parallel DataStore bindings
val preferencesModule = module {
single(named("session")) {
PreferenceDataStoreFactory.create { get<Context>().preferencesDataStoreFile("session") }
}
single(named("pendingSync")) {
PreferenceDataStoreFactory.create { get<Context>().preferencesDataStoreFile("pending_sync") }
}
}
// di/AppModules.kt — single composition point
val appModules = listOf(
// Infrastructure (concern-scoped)
firebaseModule,
databaseModule,
dispatcherModule,
preferencesModule,
connectivityModule,
workerModule,
ktorModule,
notificationModule,
repositoryModule,
// Features (feature-scoped)
authModule,
customerModule,
sellerModule,
adminModule,
)
// App.kt — the only place startKoin appears
class App : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidLogger()
androidContext(this@App)
modules(appModules)
}
}
}
Common mistakes
- A single
viewModelModulethat lists every ViewModel in the app — canonical merge-conflict magnet, and impossible to see at a glance "what does this feature own?". Split by feature. - A single
dataModulethat binds every repository and every DAO — same issue at a smaller scale. Split at least by layer (repositoryModule,databaseModule,networkModule). single<XxxViewModel>()— a ViewModel is never a singleton. UseviewModel/viewModelOf.singlewill outlive the screen and leak.- Two
DataStore<Preferences>bindings withoutnamed(...)— Koin can't disambiguate; you get a runtimeNoBeanDefFoundExceptionor, worse, the wrong instance. Always qualify parallel bindings. - Feature module importing another feature module — cross-feature coupling in the DI graph. Promote the shared binding to a concern module, or split it out (
sharedCommerceModule). get()inside a Composable body — Koin lookups are runtime; usekoinInject<T>()/koinViewModel<T>()fromkoin-androidx-compose. Or hoist the injection to the ViewModel and pass state down.startKoinin a test — collides with the runningApp's Koin. UseKoinTestRule+modules(testAuthModule, testDataModule)where the test modules override the real ones.- Forgetting to append a new module to
appModules— the app compiles, then throws at runtime the first time the missing binding is requested. Add the module toappModulesin the same commit. - Circular dependency (
customerModuledepends onsellerModulebinding X;sellerModuledepends oncustomerModulebinding Y). Koin fails at graph construction. Refactor X and Y into a shared concern module. - Injecting a
DispatcherProvideron every consumer but bindingDispatchers.IOdirectly somewhere — split brain. One canonicalDispatcherProviderbinding lives indispatcherModule.
Testing the DI graph
- Static verification: call
verify()on each feature module in a JVM unit test. Fails fast if a binding is missing. - Overriding for tests:
KoinTestRule+modules(testXxxModule)where the test module rebinds specificsingle<XxxRepository> { FakeXxxRepository() }. Prefer overriding at the repository layer, not the ViewModel layer — ViewModels should be constructed directly in tests (see[[wnb-viewmodel-test]]), not resolved through Koin. - Dynamic loading:
loadKoinModules(testAuthModule)andunloadKoinModules(testAuthModule)in@Before/@Afterwhen the test needs to swap a binding mid-suite.
Related skills
[[wnb-viewmodel-udf]]— the ViewModel shape these modules bind.[[wnb-viewmodel-test]]— why VMs are constructed directly in tests, not resolved through Koin.