Mastodon

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.
  • traceDestination emits 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 / Lazy are 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>().
  • @DefaultBinding on supertypes to avoid repeating binding() on subtypes.
  • replaces / excludes merging controls every contribution type
  • @Contributes* implies @Inject by default, eliminating redundant declarations.

Graphs

  • @GraphPrivate confines 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 @OptionalBinding for accessors.
  • Nullability is natively supported, String and String? are distinct.
  • @Binds as 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 @AssistedFactory types.
  • 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.
  • Lazy is kotlin.Lazy for idiomatic Kotlin.
  • () -> T function 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 / Lazy interop 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, @Inject placement, interop arg style.
  • reportsDestination + analyzeMetroGraph for 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; DoubleCheck for 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 generateContributionProviders keeps impl classes internal for better encapsulation and IC granularity.

MetroX

  • metrox-android: constructor-injected Android components via AppComponentFactory.
  • metrox-viewmodel + metrox-viewmodel-compose: ViewModel basic infra.
  • Built-in Circuit codegen (opt-in).
  • Runtime interop artifacts for Dagger, Guice, Javax, and Jakarta.

By the numbers

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.

No headings found in this post.