Metro Documentation

repository·main·Indexed 23 days ago

https://github.com/zacsweers/metro

A compile-time dependency injection framework for Kotlin Multiplatform that uses a Kotlin compiler plugin to provide fast, validated dependency graphs without KAPT or KSP. Includes a compiler compatibility layer to abstract evolving Kotlin compiler APIs and a comprehensive testing suite for diagnostics, IR/FIR dumps, and IDE integration.

Tokens
62.1K
Snippets
154
Records
277
Agent score
79%

What's inside Metro

  1. What is Metro?

    main
    Metro is a compile-time dependency injection framework for Kotlin Multiplatform. It is powered by a Kotlin compiler plugin (using FIR/IR code generation) and does not require KAPT or KSP. It combines patterns from Dagger (runtime efficiency), Anvil (aggregation via @ContributesTo), and kotlin-inject (Kotlin-first API with top-level function injection).
  2. Understand Metro's core DI features and semantics

    main

    Metro is a dependency injection (DI) framework built as a Kotlin compiler plugin. It provides familiar DI semantics including:

    • Injection Patterns: Constructor injection, providers, multibindings, and assisted injection.
    • Intrinsics: Support for Provider and Lazy types.
    • Validation: Full compile-time validation of the dependency graph.
    • Code Generation: Uses FIR and IR to generate code directly into existing classes. This allows for features like private @Provides declarations, injection of private member properties/functions, and the reuse of default value expressions for optional dependencies.
    • Aggregation: Supports type contribution via annotations like @ContributesTo and @ContributesBinding (similar to Anvil).
  3. Supported platforms for Metro

    main

    Metro is designed for Kotlin Multiplatform (KMP) projects:

    • Compiler Plugin: Supports all multiplatform project types.
    • Annotations (runtime artifact): Supports all common JVM, JS, and native targets.
    • Contribution hint generation: Currently not supported on native or Wasm targets until Kotlin 2.3.20-Beta1 or later (though they will still function within the same compilation).
  4. Configure Metro for AndroidX runtime tracing

    main

    When implementing runtime tracing in an Android application, follow these architectural requirements to ensure the AndroidX tracer is available before generated binding code runs:

    1. Dependency Injection: Ensure your graph factory (e.g., AppGraph.Factory) accepts a @Provides tracer: Tracer input.
    2. Ownership: The application class (e.g., MetroApp) should own the TraceDriver and TraceSink. These should be passed into the generated graph factory via driver.tracer.
    3. Profiler Integration: To ensure AndroidX profiler broadcasts use the same TraceDriver as your application, you must remove the default AndroidX profiler tracing initializer in your AndroidManifest.xml.
  5. Understand Metro validation and error reporting

    main

    Metro performs dependency graph validation at the per-graph level in the compiler IR backend. Errors are reported as structured diagnostics containing:

    • A stable diagnostic ID (e.g., [Metro/MissingBinding]).
    • A dependency chain or cycle visualization.
    • A binding trace explaining how the error was reached.
    • Annotations including help: (actionable fixes), note: (context), and docs: (reference links).

    Common error types include:

    • Missing Bindings: Occurs when no binding is found for a required type. Metro will suggest similar bindings: if a near-miss exists (e.g., different qualifier, nullability, or sub/supertype).
    • Dependency Cycles: Occurs when a circular dependency is detected. Metro draws the cycle as a closed loop and suggests breaking it using deferred types like () -> T or Lazy<T>.
  6. Interop between Metro graphs and Dagger/Kotlin-Inject components

    main

    Metro graphs can depend on, and be depended upon by, components generated by Dagger and Kotlin-Inject. This is achieved through their public accessors.

    Metro depending on Dagger

    Pass the Dagger component into a Metro @DependencyGraph.Factory:

    @DependencyGraph
    interface MetroGraph {
      val message: String
    
      @DependencyGraph.Factory
      fun interface Factory {
        fun create(
          @Includes daggerComponent: DaggerComponent
        ): MetroGraph
      }
    }
    
    @dagger.Component
    interface DaggerComponent {
      val message: String
    
      @dagger.Component.Factory
      fun interface Factory {
        fun create(@Provides message: String): DaggerComponent
      }
    }

    Dagger/Kotlin-Inject depending on Metro

    Include the Metro graph as a dependency in your Dagger or Kotlin-Inject component:

    @DependencyGraph
    interface MessageGraph {
      val message: String
    }
    
    // Dagger
    @Component(dependencies = [MessageGraph::class])
    interface DaggerComponent {
      val message: String
    
      @Component.Factory
      fun interface Factory {
        fun create(messageGraph: MessageGraph): DaggerComponent
      }
    }
    
    // kotlin-inject
    @Component
    abstract class KotlinInjectComponent(
      @Component val messageGraph: MessageGraph
    ) {
      abstract val message: String
    }
  7. Configure test behavior with Directives

    main

    Test behavior is modified using directives, which are specified as line comments at the top of the .kt test file.

    Metro provides its own directives in MetroDirectives, and you can also use standard Kotlin compiler directives. Directives can be simple boolean flags or complex instructions (e.g., disabling specific diagnostics).

    Common patterns include:

    • Boolean flags: // RENDER_DIAGNOSTICS_FULL_TEXT
    • Complex instructions: // DIAGNOSTICS: -PROVIDES_OR_BINDS_SHOULD_BE_PRIVATE
    • Pipeline control: // RUN_PIPELINE_TILL: FIR2IR
  8. Configure Scoped Suspend Bindings

    main

    Scoped suspend bindings (using @SingleIn or @DependencyGraph(scope = ...)) work like standard scoped bindings. The first successful result is cached and shared.

    Cache Semantics:

    • Concurrent callers wait and share a successful result.
    • Failed or cancelled initializations are not cached; subsequent callers will retry.
    • Circular dependencies within the same initialization chain result in an error.

    Note: Scoped suspend bindings require dev.zacsweers.metro:runtime-coroutines to be available at compile time and runtime.

    @DependencyGraph(scope = AppScope::class)
    interface AppGraph {
      suspend fun database(): Database
    
      @Provides
      @SingleIn(AppScope::class)
      suspend fun provideDatabase(): Database = openDatabase()
    }
  9. Understand SuspendDoubleCheck runtime behavior

    main

    The SuspendDoubleCheck mechanism manages concurrent access to scoped values:

    • Concurrency: It uses a private kotlinx-coroutines.Mutex. The first caller holds the mutex while invoking the delegate; subsequent callers suspend until the first attempt finishes.
    • Caching: A successful result (including null) is cached. If the delegate fails or is cancelled, the cache remains empty so future callers can retry.
    • Cancellation: Cancelling a caller waiting for the mutex does not cancel the caller currently invoking the delegate.
    • Modes:
      • suspendLazy(SYNCHRONIZED): Uses SuspendDoubleCheck (mutex-based).
      • PUBLICATION: Runs several initializers at once and publishes the first result. Uses compare-and-set on JVM/Native; checks for overlap at suspension points on JS/Wasm.
      • NONE: Caches the result only if calls do not overlap; does not coordinate concurrent callers.
  10. Use @BindingContainer to create reusable binding units

    main

    A @BindingContainer is a class, object, or interface used to group @Provides or @Binds declarations. They are not complete dependency graphs themselves, but rather reusable, composable units intended to be included in a complete @DependencyGraph.

    Unlike full graphs, the public accessors of a @BindingContainer are not read; only the @Binds and @Provides declarations within them are processed by Metro.

    @BindingContainer
    class NetworkBindings(private val baseUrl: String) {
      @Provides fun provideHttpClient(): HttpClient = HttpClient(baseUrl)
    }
  11. Scope compatibility and error rules

    main

    Metro enforces strict rules regarding scope matching to ensure graph integrity. You will encounter errors in the following scenarios:

    1. Unscoped graph accessing scoped bindings: An unscoped @DependencyGraph cannot access a class or provider that is marked with a scope.
    2. Scope mismatch: A scoped graph cannot access a scoped binding if the scopes do not match. For example, an AppGraph scoped to AppScope cannot access a class scoped to UserScope.

    Multiple Scopes: Like Dagger, a single graph can support multiple scopes. You can annotate a graph with multiple scope annotations to allow it to satisfy dependencies from different scope levels.

    @Scope annotation class Singleton
    
    @Singleton
    @SingleIn(AppScope::class)
    @DependencyGraph
    interface AppGraph {
      // This is ok
      val exampleClass: ExampleClass
    }
    
    @SingleIn(AppScope::class)
    @Inject
    class ExampleClass