<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged di</title>
<link>https://coderlegion.com/tag/di</link>
<description>Powered by Question2Answer</description>
<item>
<title>Drop the Clutch: Three Metro DI Patterns Every KMP Developer Should Know - Part 2</title>
<link>https://coderlegion.com/19771/drop-the-clutch-three-metro-di-patterns-every-kmp-developer-should-know-part-2</link>
<description>&lt;p&gt;In &lt;a rel=&quot;nofollow&quot; href=&quot;https://coderlegion.com/19769/drop-the-clutch-three-metro-di-patterns-every-kmp-developer-should-know-part-1&quot;&gt;Part 1&lt;/a&gt;, I covered Metro's fundamentals: how it differs from Koin Annotations and Dagger, the basic setup, and the &lt;code&gt;@ContributesBinding&lt;/code&gt; pattern for everyday features. This part picks up where that left off, with two patterns that push past the basics — platform-specific graph splitting and scoped lifetimes with runtime values.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;feature-2-notification-permission&quot;&gt;Feature 2: Notification permission&lt;/h3&gt;
&lt;p&gt;This is where Metro in a multiplatform project gets genuinely tricky, and where the constraint that makes it correct becomes obvious.&lt;/p&gt;
&lt;p&gt;On Android, requesting notification permission requires &lt;code&gt;Context&lt;/code&gt;, which goes to &lt;code&gt;ActivityCompat.requestPermissions&lt;/code&gt;. On iOS, you use &lt;code&gt;UNUserNotificationCenter&lt;/code&gt;. Neither type exists in &lt;code&gt;commonMain&lt;/code&gt;. The interface for the provider lives there, but the implementations are entirely platform-specific. If you want a deep dive into cross-platform notifications with KMP, I covered the full setup in a &lt;a rel=&quot;nofollow&quot; href=&quot;/posts/notifications-kmp&quot;&gt;previous article&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The instinct is to put &lt;code&gt;@DependencyGraph&lt;/code&gt; on the common interface and let platform graphs extend it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;// WRONG: fails to compile on the iOS target
// commonMain/di/AppGraph.kt
@DependencyGraph(scope = AppScope::class)
interface AppGraph {
    @DependencyGraph.Factory
    fun interface Factory {
        fun create(@Provides context: Context): AppGraph // Context is an Android type
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The iOS compiler doesn't know what &lt;code&gt;Context&lt;/code&gt; is. The build fails with an unresolved reference. After that error, we can try &lt;code&gt;@GraphExtension&lt;/code&gt;. Metro provides it for extending an existing graph, but &lt;code&gt;@GraphExtension.Factory&lt;/code&gt; is a different annotation from &lt;code&gt;@DependencyGraph.Factory&lt;/code&gt;, and &lt;code&gt;createGraphFactory&lt;/code&gt; only accepts the second one. You end up with a graph you can't instantiate.&lt;/p&gt;
&lt;p&gt;I went through both of those dead ends before the correct pattern became clear: the common interface is just a plain Kotlin interface with no Metro annotations. The &lt;code&gt;@DependencyGraph&lt;/code&gt; goes only on the platform-specific implementations.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;// commonMain/di/RootGraph.kt
import dev.zacsweers.metrox.viewmodel.ViewModelGraph

// Plain Kotlin interface, no @DependencyGraph
interface RootGraph : ViewModelGraph {
    val platformNotificationProvider: PlatformNotificationProvider
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;// androidMain/di/AndroidRootGraph.kt
import dev.zacsweers.metro.DependencyGraph
import dev.zacsweers.metro.Provides

@DependencyGraph(scope = AppScope::class)
interface AndroidRootGraph : RootGraph {

    @DependencyGraph.Factory
    fun interface Factory {
        fun create(@Provides context: Context): AndroidRootGraph
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;// iosMain/di/IosRootGraph.kt
import dev.zacsweers.metro.DependencyGraph

@DependencyGraph(scope = AppScope::class)
interface IosRootGraph : RootGraph
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Creating the graph on each platform:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;// androidMain/App.kt
class App : Application() {
    val graph: RootGraph by lazy {
        createGraphFactory&amp;lt;AndroidRootGraph.Factory&amp;gt;().create(applicationContext)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;// iosMain/MainViewController.kt
fun MainViewController() = ComposeUIViewController {
    val graph = remember { createGraph&amp;lt;IosRootGraph&amp;gt;() }
    App(graph)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;createGraph&amp;lt;T&amp;gt;()&lt;/code&gt; expects &lt;code&gt;T&lt;/code&gt; to be a &lt;code&gt;@DependencyGraph&lt;/code&gt; with no required external parameters. &lt;code&gt;createGraphFactory&amp;lt;F&amp;gt;()&lt;/code&gt; expects &lt;code&gt;F&lt;/code&gt; to be a &lt;code&gt;@DependencyGraph.Factory&lt;/code&gt; inside a graph that needs those parameters. Both return a &lt;code&gt;RootGraph&lt;/code&gt;, which is what the shared &lt;code&gt;App()&lt;/code&gt; composable receives. The composable never needs to know which platform it's running on.&lt;/p&gt;
&lt;h4 id=&quot;platform-implementations&quot;&gt;Platform implementations&lt;/h4&gt;
&lt;p&gt;The notification provider expect class lives in common:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;// commonMain/core/domain/PlatformNotificationProvider.kt
import kotlinx.coroutines.flow.StateFlow

expect class PlatformNotificationProvider {
    val permissionGranted: StateFlow&amp;lt;Boolean&amp;gt;
    fun requestPermission()
    fun observeNotificationServicePermission()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The Android implementation uses &lt;code&gt;Context&lt;/code&gt; and an &lt;code&gt;ActivityHolder&lt;/code&gt;, a wrapper that &lt;code&gt;MainActivity&lt;/code&gt; updates on &lt;code&gt;onCreate&lt;/code&gt;/&lt;code&gt;onDestroy&lt;/code&gt; so DI-managed classes can reach the current &lt;code&gt;Activity&lt;/code&gt; without holding a direct reference to it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;// androidMain/feature/notifications/data/PlatformNotificationProviderImpl.kt
@Inject
@SingleIn(AppScope::class)
actual class PlatformNotificationProvider(
    private val context: Context,
    private val activityHolder: ActivityHolder
) {
    companion object {
        const val REQUEST_CODE_NOTIFICATIONS = 1001
    }

    private val _permissionGranted = MutableStateFlow(false)
    actual val permissionGranted: StateFlow&amp;lt;Boolean&amp;gt; = _permissionGranted

    actual fun requestPermission() {
        val activity = activityHolder.get() ?: return
        ActivityCompat.requestPermissions(
            activity,
            arrayOf(Manifest.permission.POST_NOTIFICATIONS),
            REQUEST_CODE_NOTIFICATIONS
        )
    }

    actual fun observeNotificationServicePermission() {
        _permissionGranted.value = ContextCompat.checkSelfPermission(
            context, Manifest.permission.POST_NOTIFICATIONS
        ) == PackageManager.PERMISSION_GRANTED
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Android &lt;/th&gt;&lt;th&gt;iOS&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;img src=&quot;/assets/drop-the-clutch-notifications-android.gif&quot; alt=&quot;Notification permission dialog being requested and granted&quot;&gt; &lt;/td&gt;&lt;td&gt;&lt;img src=&quot;/assets/drop-the-clutch-notifications-ios.gif&quot; alt=&quot;Notification permission dialog being requested and granted&quot;&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Android doesn't deliver permission results to DI-managed classes. They come back to &lt;code&gt;Activity.onRequestPermissionsResult&lt;/code&gt;. The pattern that works: &lt;code&gt;MainActivity&lt;/code&gt; calls &lt;code&gt;observeNotificationServicePermission()&lt;/code&gt; in the callback, the provider re-reads the system state, and the &lt;code&gt;MutableStateFlow&lt;/code&gt; updates. Everything else observes the flow.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;// androidMain/MainActivity.kt
override fun onRequestPermissionsResult(
    requestCode: Int,
    permissions: Array&amp;lt;out String?&amp;gt;,
    grantResults: IntArray,
    deviceId: Int
) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults, deviceId)
    if (requestCode == PlatformNotificationProviderImpl.REQUEST_CODE_NOTIFICATIONS) {
        graph.platformNotificationProvider.observeNotificationServicePermission()
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The iOS implementation uses &lt;code&gt;UNUserNotificationCenter&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The error you get if you annotate the common graph isn't Metro being fussy. It's the compiler catching a design that would break the iOS build entirely. The plain interface isn't a workaround. It's the correct model.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;feature-3-chat-room&quot;&gt;Feature 3: Chat room&lt;/h3&gt;
&lt;p&gt;This is the pattern I wanted to understand before committing to Metro seriously. The first two features use &lt;code&gt;AppScope&lt;/code&gt; for everything, which makes sense: repositories and providers live for the app's lifetime. But not everything does.&lt;/p&gt;
&lt;p&gt;A WebSocket isn't a data object. It connects on creation and needs to be explicitly closed. Keeping it in &lt;code&gt;AppScope&lt;/code&gt; means the socket stays open for the entire app lifetime, even when the user is on a completely different screen. That's almost never what you want for a real-time connection.&lt;/p&gt;
&lt;p&gt;A scope is a lifetime, not a namespace. The &lt;code&gt;ChatScope&lt;/code&gt; exists because the socket needs to be opened when the chat screen opens and closed when it leaves. And because every chat room is a different conversation, there's a second problem to solve: the conversation ID is only known at runtime, when the user taps a row. Metro needs to receive it and make it available to anything in the scope.&lt;/p&gt;
&lt;p&gt;Here's how the two scopes relate in the demo:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;AppScope  (AndroidRootGraph / IosRootGraph)
├── ConversationRepository       singleton, lives for the app's lifetime
├── PlatformNotificationProvider singleton, lives for the app's lifetime
├── AppViewModelFactory
│
└── ChatScope  (ChatGraph) ──── created via ChatGraph.Factory at navigation time
    ├── ConversationId           runtime value, injected through the factory parameter
    ├── ChatSocket               singleton within this scope
    └── ChatViewModelFactory
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;ChatScope&lt;/code&gt; is nested inside &lt;code&gt;AppScope&lt;/code&gt; and inherits all of its bindings. &lt;code&gt;ChatGraph&lt;/code&gt; uses &lt;code&gt;@GraphExtension&lt;/code&gt; for exactly that reason. Anything in &lt;code&gt;ChatScope&lt;/code&gt; can ask for &lt;code&gt;ConversationRepository&lt;/code&gt; or &lt;code&gt;PlatformNotificationProvider&lt;/code&gt; and Metro will resolve them from the parent graph. The reverse isn't true: &lt;code&gt;AppScope&lt;/code&gt; knows nothing about &lt;code&gt;ChatSocket&lt;/code&gt; or &lt;code&gt;ConversationId&lt;/code&gt;.&lt;/p&gt;
&lt;h4 id=&quot;the-scope-and-the-socket&quot;&gt;The scope and the socket&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;// commonMain/di/ChatScope.kt
import dev.zacsweers.metro.Scope

@Scope
annotation class ChatScope
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The conversation ID is a runtime value, so it gets a proper type:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;// commonMain/feature/chat/domain/ConversationId.kt
@JvmInline
value class ConversationId(val value: String)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A value class, different from a raw &lt;code&gt;String&lt;/code&gt;, keeps the binding unambiguous. Metro identifies bindings by type, so &lt;code&gt;ConversationId&lt;/code&gt; and &lt;code&gt;String&lt;/code&gt; are different things in the graph.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;// commonMain/feature/chat/data/ChatSocket.kt
@Inject
@SingleIn(ChatScope::class)
class ChatSocket(val conversationId: ConversationId) {

    private val _messages = MutableStateFlow&amp;lt;List&amp;lt;ChatMessage&amp;gt;&amp;gt;(emptyList())
    val messages: StateFlow&amp;lt;List&amp;lt;ChatMessage&amp;gt;&amp;gt; = _messages

    private val _isTyping = MutableStateFlow(false)
    val isTyping: StateFlow&amp;lt;Boolean&amp;gt; = _isTyping

    private var job: Job? = null

    init {
        job = CoroutineScope(Dispatchers.Default).launch {
            var lap = 1
            while (true) {
                delay(2_000)
                _isTyping.value = true
                delay(1_000)
                _isTyping.value = false
                _messages.update {
                    it + ChatMessage(sender = &quot;Engineer&quot;, text = &quot;Lap $lap: pace looks good.&quot;)
                }
                lap++
            }
        }
    }

    fun close() {
        job?.cancel()
        job = null
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;ChatSocket&lt;/code&gt; declares &lt;code&gt;ConversationId&lt;/code&gt; as a constructor parameter. Metro will inject it — no special annotation needed on the parameter itself. The binding just has to exist somewhere in the scope, and the factory is where it comes from.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;@SingleIn(ChatScope::class)&lt;/code&gt; means one &lt;code&gt;ChatSocket&lt;/code&gt; per &lt;code&gt;ChatGraph&lt;/code&gt;. Not one per app, one per chat session. Navigate away and come back, and you get a fresh socket with its own conversation ID.&lt;/p&gt;
&lt;h4 id=&quot;the-graph-extension&quot;&gt;The graph extension&lt;/h4&gt;
&lt;p&gt;The chat graph uses &lt;code&gt;@GraphExtension&lt;/code&gt; rather than &lt;code&gt;@DependencyGraph&lt;/code&gt;. The difference is that &lt;code&gt;@GraphExtension&lt;/code&gt; extends an existing parent graph and inherits all of its bindings. &lt;code&gt;@DependencyGraph&lt;/code&gt; creates a standalone graph that knows nothing about its caller.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;// commonMain/di/ChatGraph.kt
import dev.zacsweers.metro.GraphExtension
import dev.zacsweers.metro.Provides
import dev.zacsweers.metrox.viewmodel.ViewModelGraph

@GraphExtension(scope = ChatScope::class)
interface ChatGraph : ViewModelGraph {
    val chatSocket: ChatSocket

    @ContributesTo(AppScope::class)
    @GraphExtension.Factory
    fun interface Factory {
        fun create(@Provides conversationId: ConversationId): ChatGraph
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;@Provides&lt;/code&gt; on the factory parameter is Metro's answer to runtime injection. When &lt;code&gt;create(conversationId)&lt;/code&gt; is called, Metro takes that value and makes it available as a binding for the entire &lt;code&gt;ChatScope&lt;/code&gt;. &lt;code&gt;ChatSocket&lt;/code&gt; asks for &lt;code&gt;ConversationId&lt;/code&gt; in its constructor; Metro delivers the one passed into the factory. No extra wiring, no manual passing.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ConversationId&lt;/code&gt; isn't being threaded through a call chain. It's a graph-level binding. Any class in &lt;code&gt;ChatScope&lt;/code&gt; can list it as a constructor parameter and get the right one automatically. The &lt;code&gt;ChatGraph&lt;/code&gt; instance is this specific conversation. Add a read-receipt tracker or a typing service later and they get the ID for free, for as long as the socket is alive.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;@ContributesTo(AppScope::class)&lt;/code&gt; on the factory tells Metro to aggregate it into the root graph automatically, so the root graph exposes &lt;code&gt;chatGraphFactory&lt;/code&gt; without needing to declare it explicitly.&lt;/p&gt;
&lt;h4 id=&quot;the-entry-composable&quot;&gt;The entry composable&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;// commonMain/feature/chat/presentation/ChatEntry.kt
@Composable
fun ChatEntry(
    factory: ChatGraph.Factory,
    conversationId: ConversationId,
    onBackClick: () -&amp;gt; Unit
) {
    val graph = remember { factory.create(conversationId) }

    DisposableEffect(Unit) {
        onDispose { graph.chatSocket.close() }
    }

    CompositionLocalProvider(LocalMetroViewModelFactory provides graph.metroViewModelFactory) {
        ChatScreen(onBackClick = onBackClick)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;remember { factory.create(conversationId) }&lt;/code&gt; creates the graph once for the lifetime of this composable. The &lt;code&gt;conversationId&lt;/code&gt; comes from the navigation back-stack entry and is fixed for the session, which is why &lt;code&gt;remember&lt;/code&gt; with no key is correct here. &lt;code&gt;DisposableEffect&lt;/code&gt; with &lt;code&gt;onDispose&lt;/code&gt; calls &lt;code&gt;close()&lt;/code&gt; when the composable leaves the composition. &lt;code&gt;CompositionLocalProvider&lt;/code&gt; swaps the ViewModel factory to the one backed by &lt;code&gt;ChatScope&lt;/code&gt;, so &lt;code&gt;metroViewModel()&lt;/code&gt; calls inside &lt;code&gt;ChatScreen&lt;/code&gt; resolve from &lt;code&gt;ChatGraph&lt;/code&gt;, not the root &lt;code&gt;AppScope&lt;/code&gt; factory.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/drop-the-clutch-chat.gif&quot; alt=&quot;Chat screen: typing indicator followed by incoming message&quot;&gt;&lt;/p&gt;
&lt;p&gt;The socket starts emitting as soon as &lt;code&gt;ChatEntry&lt;/code&gt; enters the composition. While the screen is open, a new message arrives every three seconds, preceded by a one-second typing indicator. The moment you navigate away, &lt;code&gt;onDispose&lt;/code&gt; fires, &lt;code&gt;close()&lt;/code&gt; cancels the coroutine, and it stops. Navigate back to a different conversation and you get a fresh &lt;code&gt;ChatGraph&lt;/code&gt;, a fresh &lt;code&gt;ChatSocket&lt;/code&gt;, and a new &lt;code&gt;ConversationId&lt;/code&gt;, all from a single &lt;code&gt;factory.create(newId)&lt;/code&gt; call.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;AppScope&lt;/code&gt; would mean the socket survives every screen transition for the app's entire lifetime. &lt;code&gt;ChatScope&lt;/code&gt; means it lives exactly as long as the chat screen. The &lt;code&gt;@Provides&lt;/code&gt; on factory parameter means runtime values flow into the scope cleanly, without leaking Android lifecycle details into the graph definition.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;gotchas&quot;&gt;Gotchas&lt;/h3&gt;
&lt;p&gt;Most of these I hit personally while building the demo. Metro's error messages are generally good, but a few failure modes produce errors that point at the wrong place. You see the interface in the message, not the implementation that caused the problem. That's the pattern to watch for.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;internal&lt;/code&gt; on &lt;code&gt;@ContributesBinding&lt;/code&gt; classes.&lt;/strong&gt;&lt;br&gt;
If a contributed class is &lt;code&gt;internal&lt;/code&gt; (usually in a multi-module project), Metro can't aggregate it across module boundaries. The reason: Metro's compiler plugin generates glue code that references your class by name. If that class is &lt;code&gt;internal&lt;/code&gt;, the generated code sits outside the declaring module's visibility boundary and can't see it. The build succeeds, but you'll get a &lt;code&gt;MissingBinding&lt;/code&gt; error naming the interface, not the implementation. This one is hard to trace the first time. Keep contributed classes &lt;code&gt;public&lt;/code&gt; (the default) or at least within the same Gradle module as every class that needs them.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;@DependencyGraph&lt;/code&gt; cannot extend &lt;code&gt;@DependencyGraph&lt;/code&gt;.&lt;/strong&gt;&lt;br&gt;
Metro rejects this at compile time: &quot;Graph class may not directly extend graph class.&quot; The plain-interface pattern from Feature 2 is the correct solution: annotate only the platform-specific leaf implementations.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;createGraph&lt;/code&gt; vs &lt;code&gt;createGraphFactory&lt;/code&gt;.&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;createGraph&amp;lt;T&amp;gt;()&lt;/code&gt; expects &lt;code&gt;T&lt;/code&gt; to be a &lt;code&gt;@DependencyGraph&lt;/code&gt; with no required external parameters. &lt;code&gt;createGraphFactory&amp;lt;F&amp;gt;()&lt;/code&gt; expects &lt;code&gt;F&lt;/code&gt; to be a &lt;code&gt;@DependencyGraph.Factory&lt;/code&gt;. These are different annotations; mixing them produces a compiler error that points at the right place, but the fix isn't obvious until you understand the split.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;@GraphExtension&lt;/code&gt; vs &lt;code&gt;@DependencyGraph&lt;/code&gt; for child graphs.&lt;/strong&gt;&lt;br&gt;
Use &lt;code&gt;@GraphExtension&lt;/code&gt; when the child graph should inherit parent bindings, which is the right choice for a scoped feature graph like &lt;code&gt;ChatGraph&lt;/code&gt;. Use &lt;code&gt;@DependencyGraph&lt;/code&gt; only for the root platform graphs. Mixing them up produces a graph that either can't see parent bindings or can't be instantiated correctly.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Scope mismatch.&lt;/strong&gt;&lt;br&gt;
A class scoped to &lt;code&gt;ChatScope&lt;/code&gt; that gets injected into something in &lt;code&gt;AppScope&lt;/code&gt; produces a &lt;code&gt;MissingBinding&lt;/code&gt; error at compile time. The error names the interface, not the implementation. When you see it, check whether the implementation is scoped to a graph the requester can't reach.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Manifest permissions are a separate concern.&lt;/strong&gt;&lt;br&gt;
This is more an Android issue. Metro wires up a class that calls &lt;code&gt;requestPermissions&lt;/code&gt; correctly. But if &lt;code&gt;POST_NOTIFICATIONS&lt;/code&gt; isn't declared in &lt;code&gt;AndroidManifest.xml&lt;/code&gt;, the dialog won't appear and there's no error anywhere. Always check the manifest first if a permission flow does nothing.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;wrapping-up&quot;&gt;Wrapping up&lt;/h3&gt;
&lt;p&gt;Metro moves dependency graph validation from runtime to compile time, and that shift changes how you think about DI mistakes. The patterns here cover most of the ground: binding contributions for everyday features, the platform graph split for anything that needs Android or iOS types, and child scopes for dependencies with a real lifecycle, including runtime values like a conversation ID flowing in through a factory parameter. Get those three things in place and the rest of the wiring tends to solve itself.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The paddle shifters don't make you a faster driver. But they do free up your hands for the things that actually determine where you finish.&lt;/em&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;The full demo for this article is available on &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/kmpbits/metroDIDemo&quot;&gt;GitHub&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The KMP Bits app is available on &lt;a rel=&quot;nofollow&quot; href=&quot;https://apps.apple.com/pt/app/bitsreader/id6755438235&quot;&gt;App Store&lt;/a&gt; and &lt;a rel=&quot;nofollow&quot; href=&quot;https://play.google.com/store/apps/details?id=com.joel.bitsreader&quot;&gt;Google Play&lt;/a&gt;, built entirely with KMP.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/19771/drop-the-clutch-three-metro-di-patterns-every-kmp-developer-should-know-part-2</guid>
<pubDate>Fri, 05 Jun 2026 19:03:27 +0000</pubDate>
</item>
<item>
<title>Drop the Clutch: Three Metro DI Patterns Every KMP Developer Should Know - Part 1</title>
<link>https://coderlegion.com/19769/drop-the-clutch-three-metro-di-patterns-every-kmp-developer-should-know-part-1</link>
<description>&lt;p&gt;In a GT3 car, you have a choice. You can drive with a traditional manual gearbox: clutch pedal, H-pattern shifter, full control over every gear change. Or you can switch to the sequential paddle shifters that most modern GT3 cars run. The car is still yours to drive. The racing line, the braking points, the tyre management. All of that stays with you. The only thing that changes is that you stop managing something that was never really the point.&lt;/p&gt;
&lt;p&gt;I've been running Koin in a multi-module Compose Multiplatform project for a while, and for a long time it felt like the manual gearbox. Not painful exactly, just present. Every new dependency meant another &lt;code&gt;module { }&lt;/code&gt; block somewhere, another &lt;code&gt;get()&lt;/code&gt; in the factory lambda, another place where the compiler had nothing to say if I forgot to wire something. The error came at runtime, and usually in a context that made it harder to trace than it should have been.&lt;/p&gt;
&lt;p&gt;Metro is the paddle shifters. You annotate your classes, define a graph interface, and the Kotlin compiler plugin generates the wiring at build time. If something is missing from the graph, the build fails. Not the app launch. The build.&lt;/p&gt;
&lt;p&gt;I already covered Koin Annotations in a &lt;a rel=&quot;nofollow&quot; href=&quot;/posts/koin-annotations&quot;&gt;previous article&lt;/a&gt;. What follows isn't a migration guide. It's a look at three specific patterns that come up in every real KMP project and where Metro's behaviour changed how I think about dependency injection. This is Part 1 — setup, the basics of Metro's model, and the binding pattern you'll use for most features. Part 2 covers platform-specific graphs and scoped lifetimes.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-metro-is&quot;&gt;What Metro is&lt;/h3&gt;
&lt;p&gt;Metro is a compile-time dependency injection framework for Kotlin Multiplatform. It ships as a Kotlin compiler plugin, not an annotation processor or a KSP plugin. Code generation happens in the compiler's FIR/IR pipeline directly.&lt;/p&gt;
&lt;p&gt;The mental model draws from three places: &lt;code&gt;@Inject&lt;/code&gt;, &lt;code&gt;@Provides&lt;/code&gt;, and the scope system come from Dagger; the interface-based graph definition comes from kotlin-inject; and the contribution aggregation system comes from Anvil. If you've worked with any of those three, the ideas transfer quickly.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;metro-koin-annotations-and-dagger&quot;&gt;Metro, Koin Annotations, and Dagger&lt;/h3&gt;
&lt;p&gt;Before getting into the patterns, it's worth placing Metro relative to the tools you've probably already used.&lt;/p&gt;
&lt;h4 id=&quot;koin-annotations&quot;&gt;Koin Annotations&lt;/h4&gt;
&lt;p&gt;The comparison people reach for first is Koin Annotations, because both claim compile-time safety. I want to be clear about something: these are not competing answers to the same question. They solve the same problem, both do it well, and the choice comes down to what your project actually needs.&lt;/p&gt;
&lt;p&gt;Koin Annotations uses KSP to generate source files during the build. It catches graph errors in the KSP phase, which is earlier than runtime, but the Koin service locator container still exists underneath. If something slips through, the app can still throw on startup. The generated &lt;code&gt;.kt&lt;/code&gt; files land in &lt;code&gt;build/generated/ksp/&lt;/code&gt;, and you need KSP configured and on the right classpath. For most projects, that's no trouble at all. Koin is mature, widely used, and well documented.&lt;/p&gt;
&lt;p&gt;Metro works at a different level. It's a compiler plugin operating on the IR representation of your code, the same layer where kotlinx.serialization and the Compose compiler plugin live. There are no generated source files, no &lt;code&gt;build/generated/&lt;/code&gt; directory, no KSP classpath to manage. The output is direct constructor calls baked into bytecode. Metro has no runtime container. If the graph is invalid, the build doesn't produce a binary.&lt;/p&gt;
&lt;p&gt;The trade-offs are real on both sides. Metro is strictly static, so dynamic or conditional bindings that Koin handles without friction are outside Metro's scope. And Metro is a newer library, primarily maintained by one person. Koin has years of production use behind it.&lt;/p&gt;
&lt;p&gt;My take: if your team already knows Koin, or you need dynamic binding flexibility, stick with Koin Annotations. If you want the strictest compile-time guarantee with no runtime container underneath, Metro is the right call.&lt;/p&gt;
&lt;h4 id=&quot;if-you-know-dagger&quot;&gt;If you know Dagger&lt;/h4&gt;
&lt;p&gt;If you've used Dagger before, Metro will feel familiar in the right places. &lt;code&gt;@Inject&lt;/code&gt;, &lt;code&gt;@Provides&lt;/code&gt;, scope annotations, and compile-time graph validation are all there. The graph interface replaces Dagger's &lt;code&gt;@Component&lt;/code&gt;, &lt;code&gt;@DependencyGraph.Factory&lt;/code&gt; replaces &lt;code&gt;@Component.Builder&lt;/code&gt;, and &lt;code&gt;@ContributesBinding&lt;/code&gt; replaces the manual &lt;code&gt;@Binds&lt;/code&gt; plus module wiring you'd write by hand in Dagger.&lt;/p&gt;
&lt;p&gt;Metro is a compiler plugin rather than an annotation processor, so no KAPT, no KSP, no generated source files to track. The interface-based graph definition is cleaner than Dagger's abstract class components. And Metro supports Kotlin Multiplatform natively. Dagger never did. If you've used Hilt, the &lt;code&gt;@ContributesBinding&lt;/code&gt; aggregation pattern will look familiar: Metro borrows it from Anvil, which is what Hilt builds on.&lt;/p&gt;
&lt;p&gt;Where Dagger still has an edge: it's been used in large Android codebases for over a decade, the tooling is more mature, and the error messages are better when something goes wrong. If you're on a large Android-only project with an existing Dagger graph, there's rarely a reason to migrate.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-demo&quot;&gt;The demo&lt;/h3&gt;
&lt;p&gt;The three patterns I want to show are easier to understand in context than in isolation. To make them concrete, I built a fake real-time chat app: a conversations list, a notification permission screen, and a chat room with a scoped WebSocket. No server, no real backend. Just hardcoded data and coroutine timers. Each screen demonstrates one Metro concept, and together they cover the situations you'll hit in almost any real KMP project.&lt;/p&gt;
&lt;p&gt;The article focuses entirely on the DI wiring. The UI is minimal by design.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;setup&quot;&gt;Setup&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-toml&quot;&gt;# gradle/libs.versions.toml
[versions]
metro = &quot;1.0.0&quot;

[plugins]
metro = { id = &quot;dev.zacsweers.metro&quot;, version.ref = &quot;metro&quot; }

[libraries]
metro-viewmodel = { module = &quot;dev.zacsweers.metro:metrox-viewmodel&quot;, version.ref = &quot;metro&quot; }
metro-viewmodel-compose = { module = &quot;dev.zacsweers.metro:metrox-viewmodel-compose&quot;, version.ref = &quot;metro&quot; }
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;// build.gradle.kts (root)
plugins {
    alias(libs.plugins.metro) apply false
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Apply the plugin to every module that contains Metro annotations and add the ViewModel extensions to the modules that need them:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;// composeApp/build.gradle.kts
plugins {
    alias(libs.plugins.metro)
}

kotlin {
    sourceSets {
        commonMain.dependencies {
            implementation(libs.metro.viewmodel)
            implementation(libs.metro.viewmodel.compose)
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;metrox-viewmodel&lt;/code&gt; and &lt;code&gt;metrox-viewmodel-compose&lt;/code&gt; are what give you &lt;code&gt;ViewModelGraph&lt;/code&gt;, &lt;code&gt;LocalMetroViewModelFactory&lt;/code&gt;, and &lt;code&gt;@ContributesIntoMap&lt;/code&gt; for lifecycle-aware ViewModel injection. The demo uses all three.&lt;/p&gt;
&lt;p&gt;One Gradle plugin, two extra libraries. No annotation processor configuration, no generated source directories to wire into your source sets.&lt;/p&gt;
&lt;p&gt;The project organises code by package rather than separate Gradle modules. Metro's contribution system works at the annotation level, not the module boundary, so packages are enough. A scope marker is all you need to get started:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;// commonMain/di/AppScope.kt
import dev.zacsweers.metro.Scope

@Scope
annotation class AppScope
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;feature-1-conversations-list&quot;&gt;Feature 1: Conversations list&lt;/h3&gt;
&lt;p&gt;This is the baseline. The pattern you'll use for most features in any KMP project.&lt;/p&gt;
&lt;p&gt;The interface lives in the domain layer:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;// commonMain/feature/conversations/domain/ConversationRepository.kt
interface ConversationRepository {
    suspend fun getConversations(): List&amp;lt;Conversation&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The implementation attaches itself to the graph:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;// commonMain/feature/conversations/data/FakeConversationRepository.kt
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.SingleIn
import kotlinx.coroutines.delay

@Inject
@ContributesBinding(AppScope::class)
@SingleIn(AppScope::class)
class FakeConversationRepository : ConversationRepository {

    override suspend fun getConversations(): List&amp;lt;Conversation&amp;gt; {
        delay(300) // fake network latency
        return listOf(
            Conversation(id = &quot;1&quot;, name = &quot;Garage channel&quot;, lastMessage = &quot;Tyres are warm.&quot;),
            Conversation(id = &quot;2&quot;, name = &quot;Pit wall&quot;, lastMessage = &quot;Box this lap.&quot;)
        )
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;@ContributesBinding(AppScope::class)&lt;/code&gt; tells Metro: this class implements &lt;code&gt;ConversationRepository&lt;/code&gt; and belongs to &lt;code&gt;AppScope&lt;/code&gt;. The root graph never needs to declare it. Metro aggregates everything contributed to a scope automatically at compile time. &lt;code&gt;@SingleIn(AppScope::class)&lt;/code&gt; keeps one instance alive for the app's lifetime. Remove it and Metro creates a new repository on every injection, which is almost never what you want for a repository.&lt;/p&gt;
&lt;p&gt;This is the pattern that makes multi-feature wiring low-ceremony. Each feature declares its own contributions and the root graph stays small.&lt;/p&gt;
&lt;h4 id=&quot;lifecycle-aware-viewmodel-injection&quot;&gt;Lifecycle-aware ViewModel injection&lt;/h4&gt;
&lt;p&gt;The ViewModel follows the same constructor injection pattern, but contributing it uses a map:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;// commonMain/feature/conversations/presentation/ConversationsViewModel.kt
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dev.zacsweers.metro.ContributesIntoMap
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.ViewModelKey
import dev.zacsweers.metro.binding
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch

@Inject
@ContributesIntoMap(AppScope::class, binding = binding&amp;lt;ViewModel&amp;gt;())
@ViewModelKey(ConversationsViewModel::class)
class ConversationsViewModel(
    private val repository: ConversationRepository
) : ViewModel() {

    private val _conversations = MutableStateFlow&amp;lt;List&amp;lt;Conversation&amp;gt;&amp;gt;(emptyList())
    val conversations: StateFlow&amp;lt;List&amp;lt;Conversation&amp;gt;&amp;gt; = _conversations

    init {
        viewModelScope.launch {
            _conversations.value = repository.getConversations()
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;@ContributesIntoMap&lt;/code&gt; puts this ViewModel into a map that Metro generates for the scope. The &lt;code&gt;@ViewModelKey&lt;/code&gt; annotation sets the key. That map feeds into &lt;code&gt;MetroViewModelFactory&lt;/code&gt;, an abstract class from Metro's ViewModel extensions that you subclass once per scope.&lt;/p&gt;
&lt;p&gt;Metro won't provide &lt;code&gt;MetroViewModelFactory&lt;/code&gt; automatically. It's abstract and has no &lt;code&gt;@Inject&lt;/code&gt; constructor. You bridge the gap with a small file that subclasses it for each scope:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;// commonMain/di/ViewModelFactory.kt
@Inject
@ContributesBinding(AppScope::class)
@SingleIn(AppScope::class)
class AppViewModelFactory(
    override val viewModelProviders: Map&amp;lt;KClass&amp;lt;out ViewModel&amp;gt;, () -&amp;gt; ViewModel&amp;gt;,
    override val assistedFactoryProviders: Map&amp;lt;KClass&amp;lt;out ViewModel&amp;gt;, () -&amp;gt; ViewModelAssistedFactory&amp;gt;,
    override val manualAssistedFactoryProviders: Map&amp;lt;KClass&amp;lt;out ManualViewModelAssistedFactory&amp;gt;, () -&amp;gt; ManualViewModelAssistedFactory&amp;gt;,
) : MetroViewModelFactory()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The class is &lt;code&gt;@Inject&lt;/code&gt;, so Metro can construct it. The three maps are populated from &lt;code&gt;@ContributesIntoMap&lt;/code&gt; contributions in &lt;code&gt;AppScope&lt;/code&gt;. &lt;code&gt;@ContributesBinding&lt;/code&gt; wires &lt;code&gt;AppViewModelFactory&lt;/code&gt; to the &lt;code&gt;MetroViewModelFactory&lt;/code&gt; binding in the scope. (You'll see the &lt;code&gt;ChatViewModelFactory&lt;/code&gt; equivalent in Part 2, when we introduce a second scope.)&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ViewModelGraph&lt;/code&gt; then exposes &lt;code&gt;metroViewModelFactory&lt;/code&gt;, which you provide through a &lt;code&gt;CompositionLocal&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;// commonMain/App.kt
@Composable
fun App(graph: RootGraph) {
    CompositionLocalProvider(LocalMetroViewModelFactory provides graph.metroViewModelFactory) {
        AppTheme { AppEntry() }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Inside any screen below that provider, &lt;code&gt;metroViewModel&amp;lt;ConversationsViewModel&amp;gt;()&lt;/code&gt; resolves from the map, scoped to the back stack entry, cleaned up when the screen pops. The Metro wiring doesn't touch any of that: it just provides the factory.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;A quick note on &lt;code&gt;metroViewModelFactory&lt;/code&gt;:&lt;/strong&gt; Compose's &lt;code&gt;viewModel()&lt;/code&gt; function uses reflection by default, which doesn't work with Metro since there's no runtime container. &lt;code&gt;metroViewModel&amp;lt;T&amp;gt;()&lt;/code&gt; from &lt;code&gt;metrox-viewmodel-compose&lt;/code&gt; reads &lt;code&gt;LocalMetroViewModelFactory&lt;/code&gt; and constructs ViewModels from the contributed map instead. The &lt;code&gt;MetroViewModelFactory&lt;/code&gt; subclass you write in &lt;code&gt;ViewModelFactory.kt&lt;/code&gt; is the bridge: it takes the generated provider maps as constructor arguments and wires them into the factory. Everything else works exactly as you'd expect from the Jetpack ViewModel API.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The thing that clicked for me: &lt;code&gt;@ContributesBinding&lt;/code&gt; contributes a single binding, &lt;code&gt;@ContributesIntoMap&lt;/code&gt; contributes an entry to a map. Both are aggregated automatically. Neither requires touching the root graph.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-s-next&quot;&gt;What's next&lt;/h3&gt;
&lt;p&gt;Part 1 covers the foundation: how Metro differs from Koin and Dagger, how to wire up a simple feature with &lt;code&gt;@ContributesBinding&lt;/code&gt;, and how ViewModel injection works through contributed maps.&lt;/p&gt;
&lt;p&gt;[Part 2]([PART 2 LINK]) is where it gets more interesting. It covers two patterns that every multiplatform project eventually runs into: how to split platform-specific graph implementations correctly (spoiler: the common interface carries no Metro annotations at all), and how to create a scoped graph for dependencies with a real lifecycle — including how a runtime value like a conversation ID flows into a child scope through a factory parameter.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The full demo for this article is available on &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/kmpbits/metroDIDemo&quot;&gt;GitHub&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The KMP Bits app is available on &lt;a rel=&quot;nofollow&quot; href=&quot;https://apps.apple.com/pt/app/bitsreader/id6755438235&quot;&gt;App Store&lt;/a&gt; and &lt;a rel=&quot;nofollow&quot; href=&quot;https://play.google.com/store/apps/details?id=com.joel.bitsreader&quot;&gt;Google Play&lt;/a&gt;, built entirely with KMP.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/19769/drop-the-clutch-three-metro-di-patterns-every-kmp-developer-should-know-part-1</guid>
<pubDate>Fri, 05 Jun 2026 19:03:01 +0000</pubDate>
</item>
</channel>
</rss>