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.
text1AppComponent (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,Navigatorand other global dependencies. - Platform Implementation:
- Android:
AndroidAppComponentmanaged byAppclass. - iOS:
IosAppComponentmanaged byAppDelegateclass.
- Android:
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:
AndroidUiComponentis managed inBaselineActivity. Recreated on configuration changes (rotation). - iOS:
IosUiComponentis managed by*Appclass that extendsAppand operates as a root ofContentViewhierarchy.
- Android:
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.ktUiComponent.ktPlatformComponent.kt
Adding a New Module
When you create a new feature module and want to include DI support:
-
Enable DI Plugin: Add the Baselines DI plugin to your module's
build.gradle.kts.kotlin1plugins {2 alias(libs.plugins.baselines.di)3}*This plugin configures Metro, adds all the required dependencies to allow you injecting/contributing dependencies.
-
Define Dependencies: structure your classes with
@Injector contribute them to the graph.kotlin1@ContributesBinding(AppScope::class)2class MyFeatureImpl : MyFeature { ... } -
Include in App: Add your new module as a dependency in
app/compose/build.gradle.kts.kotlin1// app/compose/build.gradle.kts2sourceSets {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
commonMaindepends only on abstractions (interfaces / data models).- Platform code provides implementations for those abstractions.
- If a dependency is declared in
PlatformComponentit is accessible anywhere in the app as a normalAppScopedependency.
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
- Metro Documentation: https://github.com/ZacSweers/metro