Files
vnidrop/.codex/skills/compose-skill/references/dependency-injection.md
Hammed Abass fd4fff2c86 docs: add compose development guidelines and skills
Added a comprehensive collection of Compose development guidelines, best practices, and skill definitions in the .codex directory.

Co-authored-by: Junie <junie@jetbrains.com>
2026-07-07 16:55:43 +02:00

4.0 KiB

Dependency Injection in Compose Projects

Shared DI guidance for Jetpack Compose and Compose Multiplatform. For framework-specific setup, see Koin or Hilt.

References:

  • Koin — Koin setup, modules, Nav 3 integration, scopes, testing
  • Hilt — Hilt setup, modules, scopes, instrumented testing

When to Use Hilt vs Koin

Criterion Hilt Koin
Platform Android-only Multiplatform (Android, iOS, Desktop, Web)
Dependency resolution Compile-time Runtime (DSL) or compile-time (Koin Annotations + KSP)
Error detection Build-time Runtime — use verify() in tests; KSP annotations add compile-time checks
Setup complexity Higher (Gradle plugins, annotations) Lower (DSL modules); annotations optional
Compose Multiplatform Not supported Full support
Navigation 3 hiltViewModel() in entry<T> blocks; multibinding entry providers — see navigation-3-di.md navigation<T> DSL + koinEntryProvider() — see navigation-3-di.md
Navigation 2 hiltViewModel() in composable destinations; graph-scoped VMs — see navigation-2-di.md koinViewModel(), koinNavViewModel(), sharedKoinViewModel() — see navigation-2-di.md

Default recommendation:

  • Android-only projects: Hilt is the default recommendation. Koin is also valid if the team prefers it or the project may become multiplatform later.
  • Compose Multiplatform projects: Use Koin — Hilt does not support non-Android targets.

For detailed setup, modules, scoping, and testing, see the dedicated references: koin.md and hilt.md. This file stays focused on the framework decision — do not duplicate implementation details here.

Shared DI Concepts

These principles apply regardless of framework choice:

Constructor injection as the default

Always inject dependencies through the constructor. Field injection (@Inject lateinit var) couples the class to the DI framework and makes testing harder.

Interface-based design

Bind interfaces to implementations — repositories, data sources, and platform services should be defined as interfaces. This enables swapping implementations in tests without mocking the DI framework.

// Define interface
interface UserRepository {
    suspend fun getUser(id: String): User
}

// Bind implementation via DI
// Koin: single<UserRepository> { UserRepositoryImpl(get()) }
// Hilt: @Binds abstract fun bind(impl: UserRepositoryImpl): UserRepository

Scope lifecycle alignment

Scope When to use Examples
Singleton Lives for app lifetime API client, database, analytics
Activity-retained Survives config changes User session, auth state
ViewModel-scoped Tied to a feature screen Feature-specific calculators, validators
Factory (new each time) Stateless or short-lived Formatters, mappers

Over-scoping wastes memory; under-scoping creates redundant instances. Match the scope to the dependency's actual lifetime.

Module organization

Organize DI modules by feature, not by type. Each feature module declares its own dependencies:

feature-product/
    ProductModule         → repository, calculator, validator, ViewModel
feature-settings/
    SettingsModule        → repository, ViewModel
core/
    CoreModule            → API client, database, platform bindings

Combine feature modules in the app module. Platform-specific bindings go in platform modules (androidMain, iosMain).

Testing principle

Swap real implementations with fakes via DI configuration — don't mock the DI framework itself. Both Koin and Hilt support module replacement in tests:

  • Koin: appModule.verify() for graph verification, module overrides in tests
  • Hilt: @TestInstallIn to replace modules, hilt-android-testing for instrumented tests

For ViewModel unit testing (framework-agnostic), see testing.md.