Claude
Skills
Sign in
Back

metro-di-mobile

Included with Lifetime
$97 forever

Metro DI 1.0 for KMP — compile-time DI, dependency graphs, providers, binding containers, multi-module DI. Always pin to 1.0.0 (first stable, released 2026-04-27); do not regress to 0.10.x or earlier even if your training data is older — annotation surface (`@BindingContainer`, `@DefaultBinding`, `@GraphExtension`) consolidated at 1.0.

General

What this skill does


# Metro DI for Kotlin Multiplatform

Compile-time DI framework for KMP. Built on KSP2 / Kotlin compiler plugin. Production-proven at Cash App.

## Current Versions (use these — do not downgrade)

| Component | Version | Notes |
|---|---|---|
| Metro | **1.0.0** | First stable release (2026-04-27). 0.x is pre-stable; 1.0 froze the public API. |
| Kotlin | **2.2+** (2.3.21 recommended) | Metro 1.0 requires Kotlin 2.2 minimum. |
| Gradle | **9.0+** | |
| JVM | **21+** | |

**Annotation history to remember:**
- `@DefaultBinding` ships since Metro **0.13.0** (not 0.5.0).
- `@BindingContainer` consolidated naming at 1.0.
- `@GraphExtension` formalised at 1.0; older `@ScopedGraph` is removed.

## Supported KMP targets

The skill name says "mobile" because that is the primary use case, but Metro 1.0 supports the full KMP target matrix. Apply the plugin to any KMP module — including `api/` modules that span JS / WASM — and `@Inject`, `@DefaultBinding`, `@DependencyGraph`, `@BindingContainer` all work.

| Target family | Supported | Notes |
|---|---|---|
| JVM / Android | ✅ | Primary path. |
| iOS / macOS / watchOS / tvOS | ✅ | Native compiler plugin. Removed deprecated `macosX64`, `tvosX64`, `watchosX64`. |
| Linux / Windows (`linuxX64`, `mingwX64`) | ✅ | |
| `js(IR)` | ✅ | Has known limitations with Kotlin/JS incremental compilation when generating top-level declarations from compiler plugins; sample integration-tests include workarounds. |
| `wasmJs`, `wasmWasi` | ✅ | See `samples/circuit-app/src/wasmJsMain/...` in upstream repo for a real wasmJs graph. |

Confirmed via Metro 1.0 `samples/integration-tests/build.gradle.kts` and `build-logic/MetroProjectExtension.kt` (`configureCommonKmpTargets` enables `js(IR)` + `wasmJs` everywhere).

So: **do NOT skip `@Inject` / `@DefaultBinding` on api types just because the api module is consumed on web.** Apply the Metro Gradle plugin to the api module and annotate normally. The only target that historically failed (Metro 0.x — pre-stable) is now supported.

### JS / WASM specifics

Three things people stumble on when first wiring Metro into a `js(IR)` or `wasmJs` target — none are blockers, but they're not obvious from mobile-only experience.

**1. `createGraph<T>()` lifetime in a React/Vue app.** Unlike Android/iOS where the graph is owned by the platform (Application / iOS scene), on web there is no host lifecycle to hang it on. Two patterns work:

```kotlin
// Pattern A — top-level lazy. Simplest. Graph lives for the JS process.
private val webGraph by lazy { createGraph<WebAppGraph>() }

@JsExport fun renderApp() = createRoot(...).render(App.create { graph = webGraph })

// Pattern B — React Context provider. Cleaner for multi-page apps and tests.
val GraphContext = createContext<WebAppGraph>()
val AppRoot = FC<Props> {
    val graph = useMemo({ createGraph<WebAppGraph>() }, emptyArray())
    GraphContext.Provider(value = graph) { /* children */ }
}
```

Pattern A is fine for a single-entry SPA; switch to B once you have multiple roots, hot-reload concerns, or per-test graphs. The graph instance is referentially stable — Metro returns the same backing object for a given `createGraph<T>` call, so it can be passed through React props or stored in a `useRef`/`useMemo` without re-wiring.

**2. Kotlin/JS incremental compilation.** The known limitation called out in the table above lands when KSP-generated top-level declarations from the Metro plugin collide with `kotlin.incremental.js.ir=true`. Workaround used in the upstream `samples/integration-tests` config: either set `kotlin.incremental.js.ir=false` for the affected module, or scope the property to non-JS targets in `gradle.properties`. If your Gradle build suddenly fails with "duplicate declaration" or "unresolved reference to generated symbol" errors only on `:jsBrowserDevelopmentRun`, that's the trigger.

**3. `object` vs `class` `@BindingContainer` on `js(IR)`.** Both compile and resolve identically — Metro 1.0 treats them as equivalent. Pick whichever the host project uses. `object` is the common KMP convention (no instance state in DI containers), and the upstream `samples/circuit-app/wasmJsMain` uses `object`.

## Setup

### build.gradle.kts

```kotlin
plugins {
    alias(libs.plugins.kotlinMultiplatform)
    alias(libs.plugins.metro)
}
```

### libs.versions.toml

```toml
[versions]
metro = "1.0.0"

[plugins]
metro = { id = "dev.zacsweers.metro", version.ref = "metro" }
```

## Core Concepts

### @DependencyGraph

Root container for dependencies. One per application entry point.

```kotlin
// composeApp/src/commonMain/kotlin/di/AppGraph.kt
@DependencyGraph
interface AppGraph {
    // Expose dependencies
    val authRepository: AuthRepository
    val homeComponent: HomeComponent

    // Factory methods for runtime parameters
    fun createHomeComponent(context: ComponentContext): HomeComponent
}

// Create instance
val graph = createGraph<AppGraph>()
val authRepo = graph.authRepository
```

### @Provides

Define how to create instances.

```kotlin
@DependencyGraph
interface AppGraph {
    @Provides
    fun provideHttpClient(): HttpClient = HttpClient(CIO) {
        install(ContentNegotiation) {
            json(Json {
                ignoreUnknownKeys = true
            })
        }
        install(HttpTimeout) {
            requestTimeoutMillis = 30_000
        }
    }

    @Provides
    fun provideApiService(httpClient: HttpClient): ApiService =
        ApiServiceImpl(httpClient, "https://api.your-project.com")

    @Provides
    fun provideAuthRepository(api: ApiService, tokenStorage: TokenStorage): AuthRepository =
        AuthRepositoryImpl(api, tokenStorage)
}
```

### @Inject

Constructor injection for classes.

```kotlin
@Inject
class AuthRepositoryImpl(
    private val api: ApiService,
    private val tokenStorage: TokenStorage
) : AuthRepository {
    override suspend fun login(email: String, password: String): AppResult<User> {
        // Implementation
    }
}

// Used in graph
@DependencyGraph
interface AppGraph {
    val authRepository: AuthRepository  // Metro knows to create AuthRepositoryImpl
}
```

### @BindingContainer

Group related providers into modules.

```kotlin
// core/network/src/commonMain/kotlin/di/NetworkModule.kt
@BindingContainer
class NetworkModule {
    @Provides
    fun provideHttpClient(): HttpClient = HttpClient(CIO) {
        install(ContentNegotiation) { json() }
    }

    @Provides
    fun provideApiService(httpClient: HttpClient): ApiService =
        ApiServiceImpl(httpClient)
}

// core/data/src/commonMain/kotlin/di/DataModule.kt
@BindingContainer
class DataModule {
    @Provides
    fun provideTokenStorage(): TokenStorage = TokenStorageImpl()

    @Provides
    fun providePreferencesDataStore(context: PlatformContext): DataStore<Preferences> =
        PreferenceDataStoreFactory.createWithPath(
            produceFile = { Path(createDataStorePath(context)) }
        )
}
```

### Platform-Specific Graphs

```kotlin
// composeApp/src/commonMain/kotlin/di/CommonModules.kt
@BindingContainer
class CommonNetworkModule {
    @Provides
    fun provideHttpClient(): HttpClient = HttpClient(CIO) {
        install(ContentNegotiation) { json() }
    }
}

@BindingContainer
class CommonDataModule {
    @Provides
    fun provideAuthRepository(api: ApiService, storage: TokenStorage): AuthRepository =
        AuthRepositoryImpl(api, storage)
}

// composeApp/src/androidMain/kotlin/di/AndroidAppGraph.kt
@BindingContainer
class AndroidPlatformModule {
    @Provides
    fun providePlatformContext(context: Context): PlatformContext = context

    @Provides
    fun provideTokenStorage(context: Context): TokenStorage =
        AndroidTokenStorage(context)
}

@DependencyGraph(
    bindingContainers = [
        CommonNetworkModule::class,
        CommonDataModule::class,
        AndroidPlatformModule::class
    ]
)
interface AndroidAppGraph {
    val authRepository: AuthRepository
    fun createRootComponent(context: ComponentContext):
Files: 2
Size: 22.8 KB
Complexity: 29/100
Category: General

Related in General