DI Overview

The project uses Metro for dependency injection. Architecture relies on compile-time graph validation, ensuring that all dependencies are satisfied before the app runs.

Why Metro instead of Koin?

While Koin is a popular runtime DI framework for Kotlin/KMP (often used with a service-locator style of resolution), Metro offers significant architectural advantages for this project:

  • Compile-time Safety: Missing dependencies cause build errors, not runtime crashes.
  • Performance: Use of generated code avoids reflection and map lookups at runtime. Metro is implemented as a Kotlin compiler plugin (FIR/IR), so DI codegen and validation happen in the compiler pipeline—no KAPT/KSP step for DI.
  • Strict Scoping: Compilation enforces correct scoping (e.g., preventing UI objects from leaking into Singleton scope).
  • Multi-module support: Feature modules don't need to know about parent components. They simply contribute bindings (@ContributesTo) and are automatically wired into the graph without manual registration in the app module.
  • Established API design: The API and concepts (@Inject, @ContributesTo, @Provides, scopes) closely mirror Dagger Hilt/Anvil, significantly reducing the learning curve for developers.

Components & Lifecycle

The DI graph is anchored by two primary components, defined in the app/compose module.

text
1AppComponent (AppScope)
2
3UiComponent (UiScope)

1. AppComponent

Scope: AppScope (Singleton)

  • Lifecycle: Created once when the application starts. Lives as long as the application process.
  • Purpose: Holds global singletons like AppDispatchers, CompositeInitializer, Navigator and other global dependencies.
  • Platform Implementation:
    • Android: AndroidAppComponent managed by App class.
    • iOS: IosAppComponent managed by AppDelegate class.

2. UiComponent

Scope: UiScope

  • Lifecycle: Created when UI entry point gets initialized; recreated on configuration changes (intentional, to keep UI-scoped objects fresh).
  • Purpose: Holds UI-related dependencies like NavGraphEntry, Playground sections, and other screen-scoped objects.
  • Dependency: UiComponent depends on AppComponent and can access app scoped deps, but not the other way around
  • Platform Implementation:
    • Android: AndroidUiComponent is managed in BaselineActivity. Recreated on configuration changes (rotation).
    • iOS: IosUiComponent is managed by *App class that extends App and operates as a root of ContentView hierarchy.

Structure & Location

The main wiring of the DI graph happens in app/compose.

  • This module acts as the DI Root.
  • It aggregates all feature modules (ui/home, toolkit/analytics, etc.) and data modules.
  • app/compose/ contains the component definitions:
    • AppComponent.kt
    • UiComponent.kt
    • PlatformComponent.kt

Adding a New Module

When you create a new feature module and want to include DI support:

  1. Enable DI Plugin: Add the Baselines DI plugin to your module's build.gradle.kts.

    kotlin
    1plugins {
    2 alias(libs.plugins.baselines.di)
    3}

    *This plugin configures Metro, adds all the required dependencies to allow you injecting/contributing dependencies.

  2. Define Dependencies: structure your classes with @Inject or contribute them to the graph.

    kotlin
    1@ContributesBinding(AppScope::class)
    2class MyFeatureImpl : MyFeature { ... }
  3. Include in App: Add your new module as a dependency in app/compose/build.gradle.kts.

    kotlin
    1// app/compose/build.gradle.kts
    2sourceSets {
    3 commonMain.dependencies {
    4 implementation(projects.ui.myFeature)
    5 }
    6}

    Crucial: If the module is not included in app/compose, the DI compiler won't see the contributed bindings, and the graph effectively won't know about them.


PlatformComponent

PlatformComponent is the explicit boundary between shared (commonMain) code and platform-specific (Android/iOS) implementations.

Goal

Expose native capabilities (Android/iOS APIs, system services, secure hardware, etc.) to shared code without leaking platform details.

Core rule

  • commonMain depends only on abstractions (interfaces / data models).
  • Platform code provides implementations for those abstractions.
  • If a dependency is declared in PlatformComponent it is accessible anywhere in the app as a normal AppScope dependency.

What belongs in PlatformComponent

Good candidates:

  • Crypto providers backed by platform keystore / Secure Enclave
  • Secure storage / keychain / keystore wrappers
  • Biometric auth / passkey integration
  • File system access, document picker, sharing
  • Push notification tokens
  • System settings / permissions / device info

Avoid putting “normal app dependencies” here (repositories, use cases, feature services). Those belong in the regular DI graph (App/UI scopes).

Ownership

  • PlatformComponent (its implementations) are owned by the supported platform layers
  • Platform dependencies are automatically wired into the app via AppComponent

Resources