Metro DI 1.0 RC1: Ending the Dagger & Anvil Era in Kotlin Multiplatform
Dagger survived because nothing better existed for Android. Metro DI 1.0-RC1 changes that premise entirely — and if youre building on Kotlin 2.0+, the question isnt whether to migrate, its how fast you can do it.
TL;DR · Quick Takeaways
• Metro DI operates as a Kotlin Compiler Plugin, transforming IR directly — no annotation processors, no separate code-gen step, zero runtime overhead.
• Anvil is not maintained for K2/KSP2; Metros @ContributesTo / @ContributesBinding pattern is the only production-grade replacement for Kotlin 2.0+ projects.
• RC1 signals API freeze on all non-experimental APIs — conservative enterprises can treat this as a soft-stable release with a clear migration window.
• Build-time benchmarks on mid-size KMP modules show IR-based wiring is 3–5× faster than equivalent KAPT processing at incremental build time.
The Architecture of Metro: Why a Compiler Plugin Trumps KSP/KAPT
Every DI framework for JVM eventually hits the same wall: the gap between what you express at the annotation layer and what the compiler actually understands. Kotlin Compiler Plugin DI closes that gap by removing the middleman entirely. Instead of generating source files that then get compiled, Metro hooks into the IR backend — the intermediate representation phase — and rewires your dependency graph as part of a single compilation pass. The result isnt just faster build. Its a fundamentally different execution model.
KAPT worked by compiling your code twice — once to produce stubs for annotation processors, then again to compile the generated sources. KSP improved things by operating on Kotlins own model instead of Java stubs, but it still produces separate .kt files that feed back into the compiler. That round-trip carries a cost that compounds on large modules. Metro skips both detours. The plugin sees your graph at the IR level and emits the wired implementation in the same pass that produces bytecode.
IR transformation versus annotation processing: the numbers
The performance delta between IR transformation and annotation processing isnt theoretical. In a KMP module with ~200 injected classes, KAPT adds roughly 8–14 seconds to a clean build on a modern M-series Mac. KSP brings that down to 3–6 seconds — meaningful, but the architecture still requires a separate code generation artifact. Metros compiler plugin approach clocks in at under 1 second of additional overhead on the same module, because there is no separate artifact to produce. The dependency graph is resolved and emitted inline. Zero-runtime overhead DI Kotlin is the accurate description here — there is no reflection, no service locator lookup, and no runtime graph initialization. Everything is resolved at compile time and baked into the bytecode.
@Inject
class AnalyticsRepository(
private val api: AnalyticsApi,
private val db: AnalyticsDatabase,
) {
// Metro resolves this graph at IR phase.
// No generated *_Factory.kt exists on disk.
// The wiring is emitted directly into bytecode.
fun track(event: Event) = api.send(db.enrich(event))
}
The factory for AnalyticsRepository exists only in bytecode — there is no AnalyticsRepository_Factory.kt polluting your source tree. That matters for project navigation, code review diffs, and build cache invalidation. Incremental builds that dont touch the injection graph produce zero generated-file changes, which means your CI cache hit rate goes up without any configuration work.
Mastering Contextual Abstraction with Kotlin 2.4 Stable Parameters I've been waiting for the death of -Xcontext-parameters since the first previews. Not because the feature was bad — it was always promising — but because "experimental"...
[read more →]What language extension actually means here
The Metro is a language extension framing isnt marketing. Compiler plugins with IR access can perform transformations that annotation processors physically cannot — like rewriting call sites across module boundaries in a single pass, or enforcing graph constraints as compilation errors rather than runtime crashes. Metros scope annotations, for example, produce actual compile-time errors if a shorter-lived dependency is injected into a longer-lived scope. With Dagger you got this as a code-gen error; with Metro its a first-class compiler diagnostic with a proper source location.
Anvil is Dead, Long Live Metro: The Kotlin 2.0+ Reality Check
Anvil was a good idea executed under constraints that no longer exist. It solved a real problem — Daggers component merging was tedious, and multi-module Android apps needed a way to contribute bindings without manually wiring everything into a root component. Anvil replacement Kotlin 2.0 was inevitable the moment K2 dropped, because Anvils entire runtime was built on the old compiler frontend. The K2 migration effort for Anvil has stalled. The repository is in maintenance mode. If youre on Kotlin 2.0+ and still relying on Anvil, youre already accumulating technical debt.
Metros contribution system didnt reinvent the wheel — it took what worked in Anvil and rebuilt it on stable foundations. The @ContributesTo annotation tells Metro to merge a module into a specific component scope. @ContributesBinding wires an implementation to an interface across module boundaries. The semantics are almost identical to Anvils contribution pattern, which makes the migration less of a rewrite and more of a find-and-replace with a few structural adjustments. The critical difference is that this now runs through IR, not through a fragile compiler plugin built on internal APIs that K2 removed.
// —— Anvil (legacy, K2-incompatible) ——————————————————————————
@ContributesBinding(AppScope::class)
class RealUserRepository @Inject constructor(
private val api: UserApi
) : UserRepository
// —— Metro DI (K2-native) ——————————————————————————————————————
@ContributesBinding(AppScope::class)
@Inject
class RealUserRepository(
private val api: UserApi
) : UserRepository
// Annotation placement differs; semantics are equivalent.
// Metro also resolves replaces= bindings at compile time.
The surface syntax is close enough that a Kotlin script can automate ~80% of the conversion. The real migration work is in module graph topology — ensuring scopes align correctly and that singleton leaks arent silently introduced during the merge. Metros strict scope checking catches mismatches at build time rather than in a late-night production crash.
Competitive Landscape: Metro DI vs. Koin vs. Dagger/Hilt
The Metro DI vs Koin vs Hilt 2026 conversation has become less nuanced than it should be. Each framework occupies a different position in the compile-safety vs. runtime-flexibility spectrum. The honest answer is that theyre not directly competing for the same workload — but if youre building a KMP product in 2026 with Kotlin 2.0+ and compile-time safety matters to you (it should), Metro is the only framework in this table that delivers all three: full KMP support, zero runtime overhead, and Kotlin 2.0 compatibility.
| Framework | Mechanism | KMP Support | Kotlin 2.0+ | Compile-time Safety | Runtime Overhead | Migration Complexity |
| :— | :— | :— | :— | :— | :— | :— |
| Metro DI 1.0-RC1 | Compiler Plugin (IR) | Full | Native | Full | Zero | Medium |
| Dagger 2 / Hilt | KAPT / KSP | Android-only | KSP2 partial | Full | Near-zero | High |
| Koin 4.x | Runtime DSL | Full | Yes | None (runtime) | Low (reflection) | Low |
| Anvil + Dagger | KAPT + plugin | Android-only | Stalled | Full | Near-zero | Medium |
| Kotlin-inject | KSP | Partial | KSP2 beta | Full | Near-zero | Medium |
Kotlin Extension Functions Pitfalls: The Hidden Cost of "Clean" Syntax Extension functions look like a gift. You take some clunky Java-style utility class, replace it with a clean .doSomething() call, and suddenly the code reads...
[read more →]Koin deserves a fair mention here. Its runtime DSL approach is genuinely useful for teams that want fast iteration speed and can accept that missing bindings produce runtime exceptions rather than build failures. For pure-KMP shared modules where youre shipping a library and dont control the host app, Koins zero-annotation approach also avoids dragging a compiler plugin into your consumers build. Thats a real architectural consideration. But for production app codebases where a missing binding is a crash in front of a user, Metros compile-time safety KMP guarantee is not optional.
The RC1 Verdict: Is it Production Ready?
Release Candidate means different things in different projects. In Metros case, Metro DI 1.0-RC1 carries a specific commitment: all non-experimental APIs are frozen. The team has explicitly stated that no breaking changes will land between RC1 and 1.0 stable on the public API surface. Thats a materially different risk profile than a beta, where the surface can shift under you between minor versions. For enterprise projects with long release cycles, RC1 is the artifact to adopt now — youll land on 1.0 stable with zero migration work required.
The honest risks are in the experimental namespace. Metro exposes some advanced features — notably certain assisted injection utilities and some Compose Multiplatform scoping helpers — under @MetroExperimental. These carry no stability guarantee. The pragmatic approach: scope your adoption to stable APIs for your production graph, and isolate experimental usage behind your own abstraction layer if you need it. Thats standard operating procedure for any framework at this maturity level.
What RC1 stability means for enterprise teams
Risk-averse architecture boards sometimes conflate not 1.0 with not production quality. For Metro, that conflating is incorrect. The codebase has been in active use in production Android and KMP applications since 0.9, and the RC1 designation reflects integration testing completeness, not code quality. The Slack engineering team and several other high-traffic Android applications have been running Metro in production since late 2024. The issue tracker at RC1 shows the open bugs are primarily documentation gaps and edge cases in experimental APIs — not graph resolution failures or correctness issues in core injection paths. Thats a healthy signal.
Strategic Migration: Decoupling from Legacy DI
Nobody migrates a production Android app from Dagger to anything else in a weekend sprint. The realistic strategy is incremental module-by-module adoption with an interop bridge that keeps your existing Dagger component tree alive while Metro progressively takes over new modules. Metro provides interop utilities specifically for this — you can expose a Dagger component as a Metro dependency source, letting newly-written Metro modules consume Dagger-provided instances during the transition period.
The practical migration sequence for a large modular app: start with leaf modules (feature modules with no downstream consumers), convert their injection graphs to Metro, then work upward toward the app-level component. The app-level Dagger component is typically the last thing to convert, because its the root of your entire graph and usually carries the most complex binding configurations. Attempting to migrate root-first is how teams end up abandoning migrations halfway. Work leaf-to-root, maintain the interop bridge, and instrument your build time at each stage — the KMP DI framework benchmark improvements compound as more modules leave the KAPT pipeline.
Kotlin API Design That Ages Well: What Your Interfaces Won't Tell You Most failures in kotlin api design don't happen at the commit that introduced the problem. They happen three months later, in a module...
[read more →]Lazy Publish and documentation continuity
One underrated operational concern during DI migrations: documentation rot. As you rename components, restructure scopes, and retire Dagger modules, your architecture docs and internal wikis go stale faster than your code. The Lazy Publish utility addresses this directly — it lets you link documentation anchors to specific source locations, so architecture notes auto-reference the live code rather than a snapshot that diverged two refactors ago. During a migration that touches dozens of modules, having your decision log anchored to the actual Metro component definitions rather than a Confluence page nobody updates is the difference between a team that knows why the graph is structured this way and one that reverse-engineers it in six months.
FAQ
01. Does Metro DI support Compose Multiplatform scoping?
Yes — Metro integrates with Compose Multiplatform through scope annotations that map directly to the composable lifecycle. You can define a @CompositionLocalScope that ties the dependency lifetime to a specific composition, which is the correct granularity for screen-level ViewModels and locally-scoped repositories. The integration is currently marked @MetroExperimental in RC1, meaning the API shape may still shift before 1.0. For production usage, the pragmatic approach is to wrap Metros Compose scoping in a thin facade of your own — that isolates your call sites from any API surface changes while still capturing the lifecycle benefits. The non-experimental injection path (constructor injection on ViewModels) is fully stable and works identically on Android and iOS targets.
02. Can Metro DI coexist with Dagger 2 during a migration?
Yes, and this is a deliberate design decision, not an afterthought. Metro ships with interop bridges that allow it to consume bindings from an existing Dagger component. You annotate your Dagger component with @MetroDependencies and Metro treats it as an external dependency source — any binding exposed on that component becomes available for injection in Metro-managed modules. The interop adds a small amount of boilerplate at the boundary point, but it means your root Dagger component can stay alive throughout the migration without requiring a big-bang conversion. The key constraint: interop bindings cross the boundary as runtime instances, so you dont get Metros compile-time graph validation for anything sourced from the Dagger side. Thats an acceptable trade-off during transition but should not become permanent architecture.
03. Why skip KSP and go straight to a Compiler Plugin for Metro DI?
KSP is still an annotation processor at heart — it reads symbol information and produces source files. That round-trip has an architectural ceiling.
Written by:
Related Articles