Wnb koin feature module
Skill wenubey/claude-android-skills/skills/wnb-koin-feature-module
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".From its SKILL.md
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.
One thing to look at
- 1 stars1 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.
SKILL.md
8.3 KB, ~1.7k tokens by cl100k_base, 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.
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.