# Metro: Compile-Time DI for Kotlin Multiplatform

> Metro is a Kotlin compiler plugin that validates dependency graphs and generates dependency injection code without KAPT or KSP. It targets Kotlin Multiplatform projects that need compile-time feedback, Kotlin-oriented APIs, and interoperability with Dagger, Guice, kotlin-inject, and inject-annotation-based code.

**ZacSweers/metro** — A multiplatform, compile-time dependency injection framework for Kotlin

- Repository: https://github.com/ZacSweers/metro
- Website: https://zacsweers.github.io/metro/
- Stars: 1,411 · Forks: 106
- Language: Kotlin
- License: Apache-2.0
- Published: 2026-10-16 · Updated: 2026-10-16 · Language: en
- Canonical page: https://hysenlabs.com/projects/zacsweers-metro

## Metro's Compiler Plugin Target

Metro addresses dependency injection in Kotlin projects where an incorrectly connected graph should be rejected by the compiler rather than discovered later during application execution. Its users are Kotlin developers building JVM, Android, JS, Wasm, or Native applications, particularly teams working across shared Kotlin Multiplatform modules. The framework presents a Kotlin-first API while retaining patterns associated with established Java dependency injection tools.

The repository is not a runtime annotation scanner that builds a graph dynamically. It is a Kotlin compiler plugin with FIR and IR code generation. The README states that Metro does not require KAPT or KSP. That distinction matters for build configuration because Metro is applied as a Gradle plugin, and its work is tied to compilation rather than a separate annotation-processing pipeline.

The plugin applies across Kotlin Multiplatform project types. This makes Metro relevant to a codebase that shares dependency graph definitions or injected components between targets. It also creates a compatibility boundary: the plugin has to work with the Kotlin compiler version used by the consuming build. The README points adopters to `docs/compatibility.md` for supported Kotlin versions, which should be checked before selecting a release.

Metro is aimed at codebases that prefer a static graph. That is a useful fit when missing bindings and invalid requests should be visible at build time. It is a poor fit when dependencies must be discovered dynamically, because the README does not describe runtime discovery or graph mutation as Metro features.

## Graph Generation Through FIR and IR

The visible entry point is a Kotlin interface marked with `@DependencyGraph`. The graph declares the dependencies that callers need and can include provider functions marked with `@Provides`. Classes marked with `@Inject` declare constructor injection. Metro compiles this graph into implementation code, and `createGraph<AppGraph>()` creates the graph object used by application code.

This is a static flow. The interface is the contract, its requested property is `UserRepository`, and the provider method returns the `Api` needed to construct that repository. `@Inject` marks `UserRepository` as injectable through its constructor parameter. The README does not show reflection, service loading, or a container populated at runtime.

Generated code is described as lean and based on patterns familiar from Dagger. The README also lists top-level function injection and optional dependencies as Kotlin-oriented capabilities. It names `@ContributesTo` and `@ContributesBinding` as aggregation features, but the quick-start example does not show how those annotations are resolved. Their exact merge rules and scope behavior therefore need to be confirmed in the aggregation documentation before use in a large module graph.

Metro also documents constructor, member, and assisted injection, coroutine bindings, deferred initialization, and scopes. These features indicate that its graph model extends beyond the minimal constructor-injection example. Their runtime objects, lifecycle expectations, and initialization ordering are not defined in the README, so those pages are necessary when the graph includes coroutines, member injection, or scoped dependencies.

## Gradle Setup and First AppGraph

The quick start begins by applying Metro's Gradle plugin. The README deliberately uses `<version>` as a placeholder, so a consuming build must replace it with the Metro version selected for that project. No separate KAPT or KSP plugin appears in the documented setup.

Apply the plugin in the `plugins` block:

```kotlin
plugins {
  id("dev.zacsweers.metro") version "<version>"
}
```

After adding the plugin, define the first graph with a requested dependency, a provider function, and an injectable class:

```kotlin
@DependencyGraph
interface AppGraph {
  val repository: UserRepository

  @Provides
  fun provideApi(): Api = ApiImpl()
}

@Inject
class UserRepository(private val api: Api)
```

This code is illustrative rather than a complete application. It assumes definitions for `Api` and `ApiImpl`. In the documented flow, `provideApi()` supplies the constructor input required by `UserRepository`, while the graph exposes the repository to application code.

Create and access the graph with:

```kotlin
val graph = createGraph<AppGraph>()
val repository = graph.repository
```

The expected result is a graph instance whose `repository` property is available without constructing the dependency chain manually. Compile the project to have Metro validate the graph. If a required binding is missing or the graph cannot satisfy a request, the README says compilation should report the dependency graph error with a diagnostic and suggestion. The exact diagnostic text is not included in the README and should not be treated as a fixed string.

## JVM to Native Target Coverage

Metro's most concrete differentiator is Kotlin Multiplatform coverage in the documentation. The README explicitly lists JVM, Android, JS, Wasm, and Native. The compiler plugin is expected to work with all Kotlin Multiplatform project types, rather than limiting graph definition to Android or the JVM.

That does not mean every injection type is equally documented here. The repository includes separate top-level modules for common runtime support, coroutines, trace support, view models, Android integrations, Compose integrations, interop packages, and a graph viewer. Those entries show several possible integration surfaces, but the README does not define their exact APIs or maturity. A team interested in `metrox-viewmodel`, `metrox-android`, or `metrox-viewmodel-compose` should use the generated API documentation and module-specific documentation rather than infer behavior from directory names.

The multiplatform design also changes adoption risk. A compiler plugin must match the Kotlin compiler used by every relevant target build, and interop annotations may differ across JVM ecosystems. The repository includes `interop-dagger`, `interop-guice`, `interop-jakarta`, and `interop-javax`, but the README only states that migration support exists. It does not quantify which Dagger or Guice features are covered, whether every annotation is supported, or whether migration can be completed incrementally in every case.

For a shared graph, the first build should therefore be treated as a compatibility checkpoint. Confirm that the selected Kotlin version appears in `docs/compatibility.md`, that the plugin resolves for the expected targets, and that any existing JVM injection annotations are handled by the relevant interop module.

## Dagger, Guice, and kotlin-inject Migration Boundary

Metro positions itself between several dependency injection approaches. The README says it combines ideas associated with Dagger, Anvil, and kotlin-inject, then gives that claim a concrete shape: compile-time validation and generated code resemble Dagger-oriented workflows, `@ContributesTo` and `@ContributesBinding` provide Anvil-style aggregation, and top-level function injection reflects a Kotlin-first design.

The documented migration path is its main practical alternative to adopting an established framework unchanged. Metro has interop areas for Dagger, Guice, `jakarta.inject`, and `javax.inject`. That can help a Kotlin codebase preserve selected bindings while moving graph creation or contributions to Metro. The README does not say that the frameworks can coexist indefinitely, nor does it document a reversible migration process.

This differs from choosing kotlin-inject because Metro adds a compiler plugin with FIR and IR generation, graph aggregation, and broader documented multiplatform targets. It differs from Guice by moving validation and implementation toward compilation rather than runtime graph assembly. Compared with Dagger, Metro offers a Kotlin-oriented API and interop modules, but the README does not provide feature-by-feature equivalence.

Migration cost is therefore tied to annotation and graph coverage. Count existing Dagger modules, components, Guice bindings, scopes, assisted injection, and cross-module contributions before replacing a graph. Then compare that inventory with the interop and aggregation documentation. A codebase that depends on an undocumented Dagger or Guice extension is a weaker candidate until the relevant integration is confirmed.

## Build Complexity and Diagnostic Payoff

Metro carries a plugin-specific maintenance cost. Gradle must apply the correct Metro release, that release must support the project's Kotlin compiler, and upgrades may require checking compiler compatibility as well as Metro's own changes. The presence of `compiler-compat/` and `compiler-tests/` indicates that compiler compatibility is a distinct part of the repository layout, but it does not prove a particular upgrade path is painless.

The benefit claimed by the project is earlier failure. Invalid graph structure can be reported during compilation rather than when a caller requests a dependency at runtime. The README also states that diagnostics include actionable suggestions. This is particularly relevant to large graphs assembled across modules, where missing contributions and incompatible requests can be costly to diagnose after deployment.

That benefit does not eliminate build-time work. FIR and IR code generation and graph validation run as part of compilation. The repository contains a `benchmark/` directory and links to performance documentation, but it gives no measurements or methodology. Claims about speed cannot be transferred to a particular application graph, project size, Kotlin version, or CI environment.

Upgrades also deserve release-note review. The listed repository releases are 1.4.5 on 2026-09-24, 1.4.4 on 2026-09-16, and 1.4.3 on 2026-09-08. The last push was on 2026-10-17, so the repository is recently active. Recent activity is not evidence that every compiler version or interop case is stable; it is only a recency fact. Check `CHANGELOG.md`, `docs/compatibility.md`, and the release notes when moving between versions.

## Apache 2.0 Terms and Adoption Boundaries

Metro is distributed under the Apache License, Version 2.0. Both the README and `LICENSE.txt` identify that license, and the repository includes its standard copyright and license notice. The license permits use and modification under its stated conditions, subject to those conditions. This review does not interpret the obligations as legal advice, and consumers should review `LICENSE.txt` for their own use.

The wrong tool is an application that requires dynamic dependency discovery or graph changes after compilation, since Metro's documented model centers on a compiler-validated graph. It is also a questionable choice for a team unwilling to accept compiler-plugin compatibility work or that cannot upgrade its Kotlin compiler when Metro requires it.

The right tool is a Kotlin codebase seeking compile-time dependency graph feedback across several Kotlin targets, especially where KAPT or KSP is undesirable. Existing Dagger, Guice, `jakarta.inject`, or `javax.inject` code may make the interop modules relevant, but their exact limits require direct verification.

The first adoption step should be a small graph containing the annotations and injection styles already used by the team. Then compare those requirements with Metro's compatibility and interop pages. That concrete check is more useful than starting with a production graph whose unsupported feature might force a redesign.

## Conclusion

Adopt Metro when a Kotlin Multiplatform codebase wants compile-time graph validation and compiler-plugin code generation without KAPT or KSP. Do not choose it solely from the README if compiler compatibility, migration scope, or exact Kotlin version support is a hard requirement. Check `docs/compatibility.md`, the interop requirements for any existing Dagger or Guice code, and the first real build of the dependency graph before committing to it.

## FAQ

### Does Metro require KAPT or KSP?

No. The README states that Metro uses FIR and IR code generation through a Kotlin compiler plugin, without KAPT or KSP.

### Which Kotlin targets does Metro support?

The README lists JVM, Android, JS, Wasm, and Native targets, and states that the compiler plugin works with Kotlin Multiplatform project types.

### Can Metro interoperate with existing Dagger or Guice code?

The README lists interop support for Dagger and Guice and says migration can be incremental. It does not document complete feature equivalence, so particular bindings and modules still need verification.

## Sources

- [License: Apache-2.0](https://github.com/ZacSweers/metro/blob/main/LICENSE)
- [Project website](https://zacsweers.github.io/metro/)
- [README](https://github.com/ZacSweers/metro/blob/main/README.md)
- [Releases](https://github.com/ZacSweers/metro/releases)
- [ZacSweers/metro on GitHub](https://github.com/ZacSweers/metro)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/zacsweers-metro
