koin-annotations icon indicating copy to clipboard operation
koin-annotations copied to clipboard

[CMP + KSP] Missing definition with interface defined in commonMain when androidMain has annotated impl

Open BigBallard opened this issue 4 months ago • 1 comments

Describe the bug I have an interface IInventoryManager that has an impl HttpInventoryManager both defined in commonMain of my CMP project. HttpInventoryManager has a dependency on a IHttpClientFactory that should be provided by a native source set (ie Android or iOS). In my adroidMain, I have an OkHttpClientFactory that is annotated as @Single. An expect module defined in commonMain and actual'd in androidMain which scans the package for the impl. KOIN_CONFIG_CHECK is set, and at build time, the following error is provided:

[ksp] --> Missing Definition for property 'httpClientFactory : com.bigballard.tool.http.IHttpClientFactory' in 'com.bigballard.tool.inventory.HttpInventoryManager'

To Reproduce Steps to reproduce the behavior:

  1. Create a CMP project with android as a target a. Setup for Koin per documentation
  2. Define an interface in commonMain and impl for interface in androidMain annotated with @Single
  3. Define an expect module class in commonMain and actual in androidMain
  4. Create class with constructor dependency in commonMain on interface and a annotate with @Single
  5. Set KOIN_CONFIG_CHECK to true in build.gradle
  6. Build project

Expected behavior That the OkHttpClientFactory definition would be picked up by koin and fulfill that IHttpClientFactory dependency.

Koin project used and used version (please complete the following information): koin-core: 4.1.0 koin-compose: 4.1.0 koin-compose-viewmodel: 4.1.0 koin-annotations: 2.1.0 koin-ksp-compiler2.1.0

Additional moduleDefinition DI.kt - commonMain

fun initDI(config: KoinAppDeclaration? = null) {
    startKoin {
        if (config != null) {
            includes(config)
        } else {
            printLogger()
        }
        modules(
            AppModule().module
        )
    }
}

AppModule.kt - commonMain

@Module(includes = [AuthModule::class, InventoryModule::class, HttpModule::class])
class AppModule

HttpModule.kt - commonMain

@Module
expect class HttpModule

HttpClientFactory.kt - commonMain

interface IHttpClientFactory

InventoryManager.kt - commonMain

interface IInventoryManager

@Single(binds = [IInventoryManager::class])
class HttpInventoryManager(private val httpClientFactory: IHttpClientFactory): IInventoryManager

HttpModule.android.kt - androidMain

@Module
@ComponentScan
actual class HttpModule

OkHttpClientFactory.kt - androidMain

@Single(binds = [IHttpClientFactory::class])
class OkHttpClientFactory(): IHttpClientFactory

BigBallard avatar Aug 30 '25 15:08 BigBallard

I encountered the exact same problem, but within a more complex module hierarchy that reveals an additional aspect of the bug.

The Specific Problem: When using a chain of module inclusions: ContextModule (with expect/actual) → is included in → ModuleA ModuleA → is included in → ModuleB

The Koin Config Check behaves inconsistently: ✅ For ModuleA, the check passes successfully ❌ For ModuleB, it fails with an error about being unable to resolve ContextWrapper

It is important to note that:

  • The ContextModule and ContextWrapper are implemented exactly as described in the official Koin documentation for platform wrappers.
  • ModuleB does not use ContextWrapper directly in its own definitions.
  • ModuleB has no dependency on the Gradle module containing the implementation of ContextWrapper.

BraveOwlet avatar Sep 05 '25 19:09 BraveOwlet