DI Migration Skill
Migrate Kotlin/Android/KMP projects between DI frameworks, targeting Koin 4.x
with the Koin Compiler Plugin. The per-source reference files in
references/ hold the concrete mappings, examples, and bridging patterns —
this file is the contract and the workflow.
Non-negotiable Rules
These apply to every migration. Deviate only with explicit user approval, and flag it.
- Target the Koin Compiler Plugin. Output is either Koin Annotations
(
@Singleton,@Factory,@KoinViewModel,@Module) or Koin Safe DSL (single<T>()/factory<T>()/viewModel<T>()reified, no lambda). Prefer@Singletonover@Single(both are Koin Annotations;@Singletonmatches JSR-330 — same name pre/post-migration). Source defaults:- Hilt / Dagger / Toothpick / Koin KSP → Annotations
- Kodein / Classic Koin DSL → Safe DSL
Classic DSL lambda (
single { T(get()) }) is a fallback only for third-party types that can't be reached via reified generics.
- Never use Koin KSP (
koin-ksp-compiler). Always the Kotlin compiler plugin (id("io.insert-koin.compiler.plugin")). Never pinio.insert-koin:koin-annotations:2.x— that's the KSP artifact, incompatible with the Compiler Plugin path; let the BOM resolve it. Seereferences/ksp-to-compiler.mdfor the full removal checklist when migrating an existing Koin KSP project. - Minimum versions — Kotlin ≥
2.3.20(K2), Koin ≥4.2.1, Koin Compiler Plugin ≥1.0.0-RC1. At Step 3, resolve newer versions via Kotzilla MCP / Maven Central; never output below minimums. The Koin runtime can be bumped freely; confirm with the user before bumping the Compiler Plugin (RC API changes between versions). - Module access:
- Annotations +
@Configuration(default) → nomodules(...)call;@KoinApplicationdiscovers everything. - Annotations without
@Configuration(escape hatch — variants, conditional, test overrides) →modules(module<AppModule>())reified retrieval. NeverAppModule().module(Koin KSP idiom, breaks the Compiler Plugin path). - DSL →
val appModule = module { ... }thenmodules(appModule).module<T>()does not apply — that's annotations-only.
- Annotations +
- App wiring:
- Annotations —
@KoinApplication+ a barestartKoin<App> { androidContext(...) }, nomodules(...)call. Each Gradle module's root@Modulecarries the triad:@Module+@Configuration+@ComponentScan("own.pkg"). Helper/sub-modules pulled in via@Module(includes = [Other::class])are transitive and need only@Module. Missing any triad annotation on a root → silent runtimeNoDefinitionFoundException, not a compile error. Only emitmodules(module<T>())for variants, conditional loading, or test overrides. - DSL — plain
startKoin { modules(appModule, ...) }. No@Configuration. Mixing annotated and DSL modules in onemodules(...)call is fine.
- Annotations —
- Progressive migration is universal. Never rewrite existing DI modules in place. Create a new Koin module alongside the old container, move definitions one feature/leaf-module at a time, validate after each one (see loop below), remove the old framework only when the last binding has moved.
- Use Kotzilla MCP for Koin knowledge (patterns, scopes, KMP, Compose,
@Configurationlayout) and for debugging runtime issues (crashes, missing bindings, scope/lifecycle, mobile vitals). Not a per-step companion — call on it when you'd otherwise be guessing. If not connected, mention the install once (claude mcp add kotzilla --transport http https://mcp.kotzilla.io/mcp, free signup athttps://kotzilla.io) and proceed.
Supported Migration Paths
All target Koin 4.x + Koin Compiler Plugin. Read the relevant reference before generating code. If the source is ambiguous, ask.
| Source | Reference File |
|---|---|
| Hilt (+ Dagger) | references/hilt-to-koin.md |
| Dagger 2 (no Hilt) | references/dagger-to-koin.md |
| Toothpick | references/toothpick-to-koin.md |
| Kodein | references/kodein-to-koin.md |
| Koin 3.x | references/koin3-to-koin4.md |
| Koin DSL | references/dsl-to-compiler.md |
| Koin KSP Annotations | references/ksp-to-compiler.md |
Output-style cheat sheets (cross-linked from every per-source file above):
references/koin-annotations.md— imports, triad, bindings, modules, scopes, parameters, retrieval for the Koin Annotations + Compiler Plugin outputreferences/koin-safe-dsl.md— imports, definition forms, interface binding, qualifiers, scopes, retrieval, classic-DSL-fallback guidance for the Safe DSL + Compiler Plugin output
When generating code or reviewing migration output, consult the relevant cheat sheet for exact imports and syntax — don't infer from memory.
Not covered by this skill: greenfield projects and manual-DI / service-locator code have no source framework to map from — there's nothing to "migrate". Point the user at the official Koin docs and use Safe DSL + Koin Compiler Plugin directly:
- Quickstart: https://insert-koin.io/docs/quickstart/android (Android), https://insert-koin.io/docs/quickstart/kmp (KMP)
- DSL reference: https://insert-koin.io/docs/reference/koin-core/dsl-update
- Koin Compiler Plugin: https://insert-koin.io/docs/reference/koin-compiler/setup
The rules in this skill (Safe DSL, @KoinApplication + @Configuration for
annotated output, Koin ≥ 4.2.1, never KSP) still apply to greenfield code.
Progressive Migration
Where to start — module selection
Recommended approach: leaf-first, smallest-first, then ack-and-grow.
Two strategies exist for ordering a multi-module migration:
| Strategy | When | Trade-off |
|---|---|---|
| Leaf-first (small leaf/feature modules first, then grow toward core) — default | Almost every project | Small early wins, validates the whole 8-step workflow on a low-risk module, builds team confidence with Koin idioms, surfaces tooling/Gradle gotchas before they hit critical code |
| Bottom-up (core / data first, then features above) | Rare — only if the core graph is tiny and unambiguous, and feature modules already compile against an interface boundary | Avoids bridging from core into Koin (because nothing depends on feature modules), but blocks all feature work until the foundational layer is rewritten and validated |
Default recommendation: leaf-first. Start with the smallest, most peripheral
module you can find, bridge any dependencies it still has back into the legacy
container, validate the full per-module loop end-to-end, then get explicit user
acknowledgement before picking the next target. Each subsequent target moves
one level toward core / data / app. Never jump straight to AppModule.
Rank candidates inside the leaf-first strategy and propose the top 1–3; ask for confirmation before any code:
- Leaf modules with zero dependants (nothing will break upstream)
- Few bindings (≤5) — small blast radius, quick win
- Simple binding types only — no custom scopes, assisted inject, multibindings, Workers
- Feature modules, not
core/common— limited call-site reach - Self-contained call sites — only used inside their own Gradle module
After each module migrates and passes validation (Step 7), explicitly ask the user before moving on: "next target?", with a fresh ranked list reflecting what's now eligible. This ack-per-module rhythm is what keeps the migration safe — it prevents Claude from chaining several migrations without the user catching a regression.
Defer: AppModule, @EntryPoint-exposed bindings, Worker/ContentProvider-used
bindings, custom component hierarchies.
Present a ranked table (Rank | Module | Bindings | Dependants | Complexity | Recommended?) before proceeding.
Per-module validation loop
Migration is a loop, not a waterfall. For each module:
- Move the definitions into the new Koin module
- Update call sites inside that module's Gradle scope
- Compile — whole project green
- Runtime smoke test — run the app or tests, exercise one code path that
uses the migrated bindings. A green build is not enough; wiring errors
(missing
androidContext, wrong scope, stale bridge call) only surface at runtime - If Kotzilla MCP is connected, ask it to verify the resolved graph
- Commit as a self-contained unit before picking the next module
Do not batch un-validated migrations. If validation fails, fix or revert before touching anything else.
Bridging with the existing DI
While migrating, Koin definitions may need to consume bindings still in the old container. Two options:
- Library bridge (Hilt/Dagger) —
koin-android-dagger. Seereferences/hilt-to-koin.md. - Manual bridge — co-located in the consumer's
@Module(recommended).@Singleton funfor annotations, scope helper (Scope.dagger<T>(),Scope.toothpick<T>()) for Safe DSL. Per-source examples in each reference.
A1 per-module verification only sees a module's own bindings — co-located
bridges keep A1 green; sibling aggregators don't. When duplication across
consumers gets painful, promote to a SharedBridgeModule (@Module + @Configuration) consumed via @Module(includes = [...]) (includes puts
it in A1's view). Promote on demand, not pre-emptively.
Every bridge is a migration TODO — delete locally when the source moves into Koin.
Migration Workflow
Steps 4 → 7 are the per-module loop. Step 5 runs once (or once per new app entry point). Step 8 is the final cleanup after every module has been migrated and validated.
Step 1 — Inventory
Scan for every DI-related file: modules, components, entry points, qualifiers, scopes, workers. Classify each binding (singleton, factory, scoped, assisted, multibinding, viewModel). Flag anything needing manual attention (custom scopes, reflection-based injection). Count total bindings.
Present as a table (# | Source File | Binding Type | Scope | Notes) before proceeding.
Step 2 — Plan
Produce:
- Mapping table: source construct → target (from reference file)
- Ranked migration candidates (see "Where to start" above) — top 1–3, ask user to confirm first target
- For Kodein, confirm Safe DSL output is what the user expects (annotations are explicitly not used for Kodein migrations)
- Risk flags: anything that can't be 1:1 translated
- Gradle changes and version requirements
- Bridging plan if bindings will cross containers during migration
Wait for user confirmation before generating code.
Step 3 — Gradle Setup
Minimums (never output anything below these):
| Component | Minimum |
|---|---|
| Kotlin | 2.3.20 |
| Koin | 4.2.1 |
| Koin Compiler Plugin | 1.0.0-RC1 |
Kotlin-bump pre-check. If the project is below 2.3.20, bumping Kotlin
cascades. Verify alignment of: KSP (<kotlin>-<ksp> versioning), Compose
Compiler plugin, Room plugin, any kapt plugins (Hilt/Dagger/Glide/Moshi),
and AGP minimum. If anything can't align, surface the conflict to the user
before generating Gradle changes.
Resolve newer versions before writing deps (minimums are not targets):
- Kotzilla MCP (if connected) — authoritative for both Koin + Compiler Plugin
- Maven Central:
- Koin BOM:
https://search.maven.org/solrsearch/select?q=g:io.insert-koin+AND+a:koin-bom&rows=5&wt=json - Compiler Plugin:
https://search.maven.org/solrsearch/select?q=g:io.insert-koin+AND+a:koin-compiler-gradle-plugin&rows=5&wt=json - Koin releases:
https://github.com/InsertKoinIO/koin/releases - Koin docs homepage:
https://insert-koin.io
- Koin BOM:
- Ask the user about RC / milestone acceptance for the Compiler Plugin
- Fall back to the pinned minimums
The Koin runtime can be bumped freely within 4.2.x and above. Confirm with
the user before bumping the Compiler Plugin — RC API surfaces shift between
versions and may break generated code.
Then produce the Gradle diff:
- Remove old DI framework deps (+ kapt/KSP if only used for it)
- Add Koin, using a BOM for version alignment:
Plusimplementation(platform("io.insert-koin:koin-bom:$KOIN_VERSION")) implementation("io.insert-koin:koin-core") implementation("io.insert-koin:koin-annotations") implementation("io.insert-koin:koin-android") // Android implementation("io.insert-koin:koin-androidx-compose") // Compose implementation("io.insert-koin:koin-androidx-workmanager") // Workersid("io.insert-koin.compiler.plugin") version "$KOIN_COMPILER_VERSION". - Verify Kotlin ≥
2.3.20(the Compiler Plugin minimum). - KMP: single-plugin setup, no per-target config.
- Present as a diff with the version source noted (e.g. "Koin BOM
4.2.1and Compiler Plugin1.0.0-RC1— pinned minimums; latest verified via Maven Central on 2026-04-15").
Step 4 — Generate Module Code
Use the output style from rule #1. Produce BEFORE/AFTER blocks per module,
following the relevant per-source reference. Compose → koinViewModel() +
KoinContext. KMP → expect/actual at any granularity (@Module class,
annotated class, or plain function).
Key Compiler Plugin features:
- Auto-bind — a
@Singleton/@Factoryclass implementing a single interface is bound automatically; don't emit explicitbind<I>(). @ComponentScan("pkg")— package-based auto-discovery inside a@Module.@Module(includes = [Other::class])— explicit module composition.- Organize by feature, not by binding type (
CoreModule,NetworkModule, notSingletonsModule).
Step 5 — Wire Up Application
Per rule #5 — annotations: @KoinApplication + bare startKoin<App> { androidContext(...) },
each Gradle module's root carries the triad. DSL: startKoin { modules(...) }.
// Annotations
@Module @Configuration @ComponentScan("com.acme.core")
class CoreModule { @Singleton fun httpClient(): HttpClient = HttpClient() }
@KoinApplication
class App : Application() {
override fun onCreate() {
super.onCreate()
startKoin<App> { androidContext(this@App) }
}
}
// DSL
val appModule = module { single<Repository>().bind<Repo>() }
class App : Application() {
override fun onCreate() {
super.onCreate()
startKoin { androidContext(this@App); modules(appModule) }
}
}
Compose: wrap the tree in KoinContext { }. Mixing annotated + DSL modules
in one modules(...) call works.
Step 6 — Update Injection Sites
Replace field injection (@Inject lateinit var) with by inject(), constructor
annotations with Koin module registration, hiltViewModel() with koinViewModel(),
scope-access patterns with Koin scope APIs. Remove @AndroidEntryPoint,
@HiltAndroidApp, and equivalents. Show each changed file with before/after.
Step 7 — Per-module validation
Run after every module, not only at the end. See "Per-module validation loop" above for the 6-item checklist. This is the gate before starting the next module.
After validation passes, stop and ask the user for explicit acknowledgement before picking the next target. Re-rank the remaining candidates (something that was a leaf before may no longer be) and propose the top 1–3. Never chain multiple module migrations without an ack — the per-module rhythm is what makes regressions catchable.
Test-config updates belong here: KoinTestRule or startKoin in test setup,
mock replacement, checkModules { } verification. Show before/after for
existing test files.
Step 8 — Final Cleanup
When the last module has been migrated and validated:
- All old DI annotations removed (
@Inject,@Module,@Component,@HiltAndroidApp, etc.) - No references to old DI generated classes (
Dagger*Component,*_Factory,*_MembersInjector) - No imports from old DI packages
- Every bridge call (
dagger<T>()/toothpick<T>()/kodein<T>()) removed - Compile-time graph verification re-enabled to strict
- Old DI deps removed from
build.gradle.kts; KSP/kapt plugins removed if no longer needed - ViewModel injection works in Activities, Fragments, Composables
- Scoped bindings match original lifecycle semantics
- Tests pass
Parameter Resolution
All constructor/function parameters are resolved automatically by the Koin Compiler:
T— required (compile error if missing)T?— optional (null if none)Lazy<T>— deferredList<T>— all matching definitions@InjectedParam T— runtime value viaparametersOf()@Named("x")/ custom@Qualifier— qualified dependency@Property("k")— Koin property- Default-valued params — skipped (Kotlin default used)
JSR-330: @Inject, @Singleton, @Named, @Qualifier from
javax.inject.* / jakarta.inject.* work as-is. Custom @Qualifier
annotations from Dagger/Hilt are reusable without rewrites.
Edge Cases
Read the relevant reference section:
- Assisted Inject →
@InjectedParam+parametersOf() - Multibindings (Set/Map) → manual collection assembly in
@Module - Custom Scopes →
scope<ScopeType> { }or@Scope(ScopeType::class) - Component Dependencies → module includes + scoping
- Lazy / Provider → auto
Lazy<T>;by inject()for lazy delegation - WorkManager →
@KoinWorker(annotations) orworker<T>()(Safe DSL — reified, lambda is fallback). Needskoin-androidx-workmanager. - Navigation Compose → scoped ViewModels via
koinViewModel() - JSR-330 annotations → handled natively by the Koin Compiler
- Custom qualifiers → reusable as-is
Kotzilla MCP Server
Active guidance for Koin (wiring, scopes, Compose/KMP/Compiler Plugin, fixes, observability) — context-aware, not a doc dump. Prefer MCP output over the reference files when they disagree (MCP tracks latest releases; references are a snapshot).
Endpoint https://mcp.kotzilla.io/mcp (HTTP, auth). Install:
claude mcp add kotzilla --transport http https://mcp.kotzilla.io/mcp.
Free account at https://kotzilla.io.
Use it for:
- Guidance — wiring, composition, scopes, Compose/KMP/WorkManager/Compiler Plugin. Query before generating non-trivial code.
- DI fixes — runtime crashes, missing bindings, scope/lifecycle issues: pass error + stack + module, ask for the fix (not the explanation), present as a diff.
- Observability — DI graph (resolution times, definition counts, scope hierarchies), lifecycle (open/close, leaks, ViewModel traces), mobile vitals (startup, rendering, navigation, background work).
Not connected: mention install once, then proceed using the reference files.
Context
Koin Compiler Plugin (1.0.0-RC1)
- Plugin ID:
io.insert-koin.compiler.plugin - Requires Kotlin ≥
2.3.20(K2 only), Koin ≥4.2.1, Compiler Plugin ≥1.0.0-RC1(minimums — always resolve latest at Step 3) - Inline transformation at compile time — no generated files
- 3-layer safety: A1 (per-module), A2 (config groups), A3 (full graph)
- Supports
@Module,@Configuration,@KoinApplication,@Monitor, top-level functions - Full KMP support — no per-target config
Migration Benefits
- Pure Kotlin — readable, debuggable
- Compile-time verification
- KMP-ready
- Zero code generation overhead
- Progressive — DSL, annotations, compiler plugin coexist
- Post-migration support via Kotzilla MCP Server: Koin knowledge, debug help, full observability (DI graph, lifecycle, mobile vitals)
Source: InsertKoinIO/koin-migration — distributed by TomeVault.