Metro 1.0.0 is out now and stable. This means that its runtime APIs (runtime, MetroX artifacts, Gradle plugin, etc.) are now API-stable unless annotated with an experimental annotation.
This is an exciting milestone! Metro's come a long way since its early days in November of 2024 as a prototype called Lattice. It's the proudest work of my career and I strongly feel this is how dependency injection in Kotlin should be.
Dozens of companies, small and large, have migrated to Metro already and are seeing amazing results in their build times when coming from traditional source generation tools. The improvements are often upwards of 50-80%. These results represent the kind of step-function improvement that dedicated DevXP teams might spend years trying to achieve.
For projects coming from runtime-based service location or manual DI, they're able to achieve all the production safety and developer productivity gains of an automated, true compile-time-validating DI system and without sacrificing build performance.
What's in the box
The headline is build performance, but Metro has grown into a pretty comprehensive framework along the way. A non-exhaustive tour:
Compiler & build performance
- Pure compiler plugin: FIR for analysis and class header code gen, IR for all other codegen. No KSP/KAPT source generation pass.
- All-in-one solution: no multiple moving parts (Dagger + Anvil + KAPT/KSP) with shifting compatibility stories. A single library means no breakages or compatibility issues.
- Native, fine-grained integration with kotlinc's incremental compilation infrastructure.
traceDestinationemits Perfetto traces of the IR pipeline.
True compile-time safety
- Full dependency graph validation at compile time, not just reachability.
- Compile-time cycle detection with the full cycle paths reported.
Provider/Lazyare recognized as valid cycle breakers. - Compile-time scope correctness validation: unscoped graphs can't consume scoped bindings, scoped graphs must declare matching scopes.
- Multibindings structurally validated; map keys cannot have duplicates, empty ones are an error by default.
- Assisted parameters matched by name between constructor and factory.
- No runtime DI machinery: no reflection, no hashmap lookups, no service locator, no global module registry, no worries.
Aggregation as a first-class citizen
@DependencyGraph(scope = ...)merges contributions directly, there are no intermediate merged-component facades.@ContributesTo,@ContributesBinding,@ContributesIntoSet,@ContributesIntoMap, plus contributed binding containers.- Generic bound types via
binding<T>(). @DefaultBindingon supertypes to avoid repeatingbinding()on subtypes.replaces/excludesmerging controls every contribution type@Contributes*implies@Injectby default, eliminating redundant declarations.
Graphs
@GraphPrivateconfines bindings to their declaring graph.- Graph extensions, including contributed extensions that materialize in a parent graph downstream.
- Optional bindings via native Kotlin default parameters, plus
@OptionalBindingfor accessors. - Nullability is natively supported,
StringandString?are distinct. @Bindsas abstract extension properties, inlined at compile time.- Dynamic graphs:
createDynamicGraph()for easy binding replacements in tests.
Injection
- Top-level function injection, preserving
@Composable,suspend, and context parameters. - Opt-in auto-generated
@AssistedFactorytypes. - Assisted parameters match by name.
- Can inject private members, functions, and constructors, and use private
@Provides. - Default-value expressions copied into generated factories and work even if they reference private APIs.
Lazyiskotlin.Lazyfor idiomatic Kotlin.() -> Tfunction providers for idiomatic Kotlin.
Multiplatform
- Metro is multiplatform-first and targets most major Kotlin multiplatform targets in its runtime artifacts + supports them on in code gen.
- Cross-module aggregation on every target (as of recent Kotlin versions).
Interop
- One-liners for Dagger, Anvil, kotlin-inject, kotlin-inject-anvil, and Guice.
- Runtime
Provider/Lazyinterop with Dagger, Javax, Jakarta, and Guice types. - Reuses existing Dagger-generated factories during migrations.
- Metro graphs and Dagger/kotlin-inject components can depend on each other via
@Includes. - Honors Anvil's
boundType,rank,ignoreQualifier, and@ContributesMultibinding.
Diagnostics
- Fully-qualified references in error output; "similar bindings" suggestions on misses.
- Configurable severity for scoped-public providers, non-public contributions, unused graph inputs,
@Injectplacement, interop arg style. reportsDestination+analyzeMetroGraphfor JSON dumps and HTML visualizations.- debug prints pseudo-Kotlin for generated IR.
- FIR diagnostics in the K2 IDE plugin (behind a registry flag).
Codegen optimizations
- Unused-binding elimination.
- Reference-count-based provider fields;
DoubleCheckfor scoped bindings. - Init statement chunking to dodge JVM method size limits.
- Graph class sharding to dodge JVM class size limits.
- Opt-in switching providers (deferred class loading).
- Opt-in
generateContributionProviderskeeps impl classes internal for better encapsulation and IC granularity.
MetroX
metrox-android: constructor-injected Android components viaAppComponentFactory.metrox-viewmodel+metrox-viewmodel-compose:ViewModelbasic infra.- Built-in Circuit codegen (opt-in).
- Runtime interop artifacts for Dagger, Guice, Javax, and Jakarta.
By the numbers
- 100k+ lines of Kotlin code
- 5 different versions of the Kotlin compiler are supported
- 7 different IDE versions are supported and tested (five Android Studio versions and two IntelliJ versions)
- 50+ external contributors
- 1000+ (non-automated) PRs, around ~30% from external contributors
- 60+ releases
What does stability mean?
Stability, right now, is purely focused on the stable ABI of the runtime artifacts.
What does future compatibility look like?
The supported compiler/IDE versions will always be a moving window, but I'm pretty pleased with how consistently wide that window has been. Metro has an advanced compatibility layer that's scaled extremely well. It also runs a thorough test matrix of different compiler versions and IDE integration tests on every CI build.
Metro's internal metadata, in theory, could be safely versioned and better allow for mixing different versions of Metro-processed code. However, that's not currently a goal. It does not include support for mixing different versions of Metro-processed code at the moment.
What's Next?
The work continues! Maintaining a compiler plugin is an evergreen project (consider sponsoring Metro!), and there are new features I want to continue building. Metro's runtime has been pretty stable the past few months, so now felt like a good time to formalize that.
Thank you to everyone that's contributed and helped this project get to where it's at. Metro wouldn't be where it is without you.
