From 3640db94025e1e0cd1d4889f7c84ac0387cec8fb Mon Sep 17 00:00:00 2001 From: jinzequn Date: Wed, 23 Oct 2024 10:42:23 +0800 Subject: [PATCH 1/9] fix flatten module order error Fixes gh-1998 --- .../kotlin/org/koin/core/module/Module.kt | 2 +- .../commonTest/kotlin/org/koin/dsl/FlattenTest.kt | 15 ++++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/projects/core/koin-core/src/commonMain/kotlin/org/koin/core/module/Module.kt b/projects/core/koin-core/src/commonMain/kotlin/org/koin/core/module/Module.kt index 430381bc6..8d4612d69 100644 --- a/projects/core/koin-core/src/commonMain/kotlin/org/koin/core/module/Module.kt +++ b/projects/core/koin-core/src/commonMain/kotlin/org/koin/core/module/Module.kt @@ -248,7 +248,7 @@ fun flatten(modules: List): Set { } // Add all the included modules to the stack if they haven't been visited yet. - for (module in current.includedModules) { + for (module in current.includedModules.asReversed()) { if (module !in flatten) { stack += module } diff --git a/projects/core/koin-core/src/commonTest/kotlin/org/koin/dsl/FlattenTest.kt b/projects/core/koin-core/src/commonTest/kotlin/org/koin/dsl/FlattenTest.kt index ab419d870..26b42f5f0 100644 --- a/projects/core/koin-core/src/commonTest/kotlin/org/koin/dsl/FlattenTest.kt +++ b/projects/core/koin-core/src/commonTest/kotlin/org/koin/dsl/FlattenTest.kt @@ -2,6 +2,7 @@ package org.koin.dsl import org.koin.core.module.flatten import kotlin.test.Test +import kotlin.test.assertEquals import kotlin.test.assertTrue class FlattenTest { @@ -12,17 +13,17 @@ class FlattenTest { val m2 = module { } val modules = m1 + m2 - assertTrue { flatten(modules) == setOf(m1, m2) } + assertSetEqualsInOrder(flatten(modules), setOf(m1, m2)) } @Test fun test_flatten_common() { val m1 = module { } val m2 = module { } - val m3 = module { includes(m1) } + val m3 = module { includes(m1, m2) } val modules = m3 + m2 - assertTrue { flatten(modules) == setOf(m3, m1, m2) } + assertSetEqualsInOrder(flatten(modules), setOf(m3, m1, m2)) } @Test @@ -32,7 +33,7 @@ class FlattenTest { val m3 = module { includes(m1) } val modules = m3 + m2 - assertTrue { flatten(modules) == setOf(m3, m1, m2) } + assertSetEqualsInOrder(flatten(modules), setOf(m3, m1, m2)) } @Test @@ -43,6 +44,10 @@ class FlattenTest { val m3 = module { includes(m1) } val modules = m3 + m2 - assertTrue { flatten(modules) == setOf(m3, m1, m4, m2) } + assertSetEqualsInOrder(flatten(modules), setOf(m3, m1, m4, m2)) + } + + private fun assertSetEqualsInOrder(actual: Set, expected: Set) { + assertEquals(expected.toList(), actual.toList()) } } From 00496cc6bef9e322ae171ab4c7f337a8fb6cbb0b Mon Sep 17 00:00:00 2001 From: Lidonis Calhau Date: Thu, 17 Jul 2025 09:29:26 +0200 Subject: [PATCH 2/9] Koin + Ktor DI 3.2 Integration Ktor DI Bridge Implementation - Added KoinDependencyMapExtension implementing Ktor 3.2's DependencyMapExtension interface - Registered via SPI in META-INF/services/io.ktor.server.plugins.di.DependencyMapExtension Koin can resolve Ktor DI dependencies via KtorDIExtension resolution extension. - There is a potential problem with runBlocking usage When dependency is not found, each library is delegating to the other in an infinite loop Fixed a problem in CoreResolver that was not resolving extensions Created a sample application to show usage. --- examples/gradle/versions.gradle | 2 +- examples/ktor-di-sample/build.gradle | 31 +++++ .../org/koin/sample/ktor/di/Application.kt | 60 ++++++++ .../org/koin/sample/ktor/di/Services.kt | 17 +++ .../koin/sample/ktor/di/ApplicationTest.kt | 60 ++++++++ examples/settings.gradle | 1 + .../org/koin/core/resolution/CoreResolver.kt | 2 +- projects/gradle/libs.versions.toml | 4 +- projects/ktor/koin-ktor/build.gradle.kts | 5 +- .../org/koin/ktor/di/KoinDependencyMap.kt | 90 ++++++++---- .../org/koin/ktor/di/KtorDIExtension.kt | 48 ++++--- .../kotlin/org/koin/ktor/plugin/KoinPlugin.kt | 7 +- ...r.server.plugins.di.DependencyMapExtension | 1 + .../org/koin/ktor/di/KtorDIBridgeTest.kt | 128 ++++++++++++++++++ 14 files changed, 402 insertions(+), 54 deletions(-) create mode 100644 examples/ktor-di-sample/build.gradle create mode 100644 examples/ktor-di-sample/src/main/kotlin/org/koin/sample/ktor/di/Application.kt create mode 100644 examples/ktor-di-sample/src/main/kotlin/org/koin/sample/ktor/di/Services.kt create mode 100644 examples/ktor-di-sample/src/test/kotlin/org/koin/sample/ktor/di/ApplicationTest.kt create mode 100644 projects/ktor/koin-ktor/src/commonMain/resources/META-INF/services/io.ktor.server.plugins.di.DependencyMapExtension create mode 100644 projects/ktor/koin-ktor/src/jvmTest/kotlin/org/koin/ktor/di/KtorDIBridgeTest.kt diff --git a/examples/gradle/versions.gradle b/examples/gradle/versions.gradle index 6237cbacf..24b9aa19f 100644 --- a/examples/gradle/versions.gradle +++ b/examples/gradle/versions.gradle @@ -7,7 +7,7 @@ ext { koin_compose_version = koin_version coroutines_version = "1.9.0" - ktor_version = "3.1.3"//"3.2.0-eap-1310" + ktor_version = "3.2.1" jb_compose_version = "1.8.0" // Test diff --git a/examples/ktor-di-sample/build.gradle b/examples/ktor-di-sample/build.gradle new file mode 100644 index 000000000..34d03e919 --- /dev/null +++ b/examples/ktor-di-sample/build.gradle @@ -0,0 +1,31 @@ +apply plugin: 'kotlin' +apply plugin: 'application' + +archivesBaseName = 'ktor-di-sample' +mainClassName = 'org.koin.sample.ApplicationKt' + +dependencies { + // Kotlin + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + implementation "io.ktor:ktor-server-netty:$ktor_version" + implementation "io.ktor:ktor-server-call-logging:$ktor_version" + implementation "io.ktor:ktor-server-di:$ktor_version" + + // Use local Koin project dependencies + implementation "io.insert-koin:koin-ktor" + implementation "io.insert-koin:koin-logger-slf4j" + implementation("org.slf4j:slf4j-api:2.0.16") + implementation "ch.qos.logback:logback-classic:1.5.16" + + testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version" + testImplementation "io.ktor:ktor-server-test-host:$ktor_version" + testImplementation "io.insert-koin:koin-test" + testImplementation "io.insert-koin:koin-test-junit4" +} + +repositories { + mavenLocal() + mavenCentral() + maven { url "https://dl.bintray.com/kotlin/kotlinx" } + maven { url "https://dl.bintray.com/kotlin/ktor" } +} \ No newline at end of file diff --git a/examples/ktor-di-sample/src/main/kotlin/org/koin/sample/ktor/di/Application.kt b/examples/ktor-di-sample/src/main/kotlin/org/koin/sample/ktor/di/Application.kt new file mode 100644 index 000000000..ea66f6eab --- /dev/null +++ b/examples/ktor-di-sample/src/main/kotlin/org/koin/sample/ktor/di/Application.kt @@ -0,0 +1,60 @@ +package org.koin.sample.ktor.di + +import io.ktor.server.application.* +import io.ktor.server.engine.* +import io.ktor.server.netty.* +import io.ktor.server.plugins.calllogging.* +import io.ktor.server.plugins.di.dependencies +import io.ktor.server.response.* +import io.ktor.server.routing.* +import org.koin.core.logger.Level +import org.koin.dsl.module +import org.koin.ktor.ext.inject +import org.koin.ktor.plugin.Koin + +fun main() { + embeddedServer(Netty, port = 8080) { + mainModule() + }.start(wait = true) +} + +fun Application.mainModule() { + install(CallLogging) + + install(Koin) { + printLogger(Level.DEBUG) + modules(module { + single { HelloServiceImpl() } + }) + } + + dependencies { + provide { KtorSpecificServiceImpl() } + } + + routing { + get("/koin") { + val helloService: HelloService by inject() // From koin + call.respondText(helloService.sayHello()) + } + + get("/ktor-di") { + val ktorService: KtorSpecificService by dependencies // From Ktor DI + call.respondText(ktorService.process()) + } + + get("/mixed-ktor-di") { + val helloService: HelloService by dependencies // From Koin via Ktor DI + val ktorService: KtorSpecificService by dependencies // From Ktor DI + + call.respondText("${helloService.sayHello()} - ${ktorService.process()}") + } + + get("/mixed-koin") { + val helloService: HelloService by inject() // From Koin + val ktorService: KtorSpecificService by inject() // From Ktor Di via Koin + + call.respondText("${helloService.sayHello()} - ${ktorService.process()}") + } + } +} \ No newline at end of file diff --git a/examples/ktor-di-sample/src/main/kotlin/org/koin/sample/ktor/di/Services.kt b/examples/ktor-di-sample/src/main/kotlin/org/koin/sample/ktor/di/Services.kt new file mode 100644 index 000000000..f0d69dae1 --- /dev/null +++ b/examples/ktor-di-sample/src/main/kotlin/org/koin/sample/ktor/di/Services.kt @@ -0,0 +1,17 @@ +package org.koin.sample.ktor.di + +interface HelloService { + fun sayHello(): String +} + +class HelloServiceImpl : HelloService { + override fun sayHello(): String = "Hello from Koin!" +} + +interface KtorSpecificService { + fun process(): String +} + +class KtorSpecificServiceImpl : KtorSpecificService { + override fun process(): String = "Processed by Ktor DI" +} \ No newline at end of file diff --git a/examples/ktor-di-sample/src/test/kotlin/org/koin/sample/ktor/di/ApplicationTest.kt b/examples/ktor-di-sample/src/test/kotlin/org/koin/sample/ktor/di/ApplicationTest.kt new file mode 100644 index 000000000..fab47717b --- /dev/null +++ b/examples/ktor-di-sample/src/test/kotlin/org/koin/sample/ktor/di/ApplicationTest.kt @@ -0,0 +1,60 @@ +package org.koin.sample.ktor.di + +import io.ktor.client.request.get +import io.ktor.client.statement.bodyAsText +import io.ktor.http.HttpStatusCode +import io.ktor.server.testing.testApplication +import org.junit.Test +import kotlin.test.assertEquals + +class ApplicationTest { + + @Test + fun `test koin endpoint`() = testApplication { + application { + mainModule() + } + + val response = client.get("/koin") + + assertEquals(HttpStatusCode.Companion.OK, response.status) + assertEquals("Hello from Koin!", response.bodyAsText()) + } + + @Test + fun `test ktor-di endpoint`() = testApplication { + application { + mainModule() + } + + val response = client.get("/ktor-di") + + assertEquals(HttpStatusCode.Companion.OK, response.status) + assertEquals("Processed by Ktor DI", response.bodyAsText()) + } + + @Test + fun `test mixed-ktor-di endpoint`() = testApplication { + application { + mainModule() + } + + val response = client.get("/mixed-ktor-di") + + assertEquals(HttpStatusCode.Companion.OK, response.status) + assertEquals("Hello from Koin! - Processed by Ktor DI", response.bodyAsText()) + } + + @Test + fun `test mixed-koin endpoint`() = testApplication { + application { + mainModule() + } + + val response = client.get("/mixed-koin") + + assertEquals(HttpStatusCode.Companion.OK, response.status) + assertEquals("Hello from Koin! - Processed by Ktor DI", response.bodyAsText()) + } + +} \ No newline at end of file diff --git a/examples/settings.gradle b/examples/settings.gradle index 3276b110a..5bc79b330 100644 --- a/examples/settings.gradle +++ b/examples/settings.gradle @@ -6,6 +6,7 @@ include 'jvm-perfs' include 'android-perfs' // ktor include 'hello-ktor' +include 'ktor-di-sample' // Compose include 'sample-android-compose' diff --git a/projects/core/koin-core/src/commonMain/kotlin/org/koin/core/resolution/CoreResolver.kt b/projects/core/koin-core/src/commonMain/kotlin/org/koin/core/resolution/CoreResolver.kt index 9b024526f..e441b9068 100644 --- a/projects/core/koin-core/src/commonMain/kotlin/org/koin/core/resolution/CoreResolver.kt +++ b/projects/core/koin-core/src/commonMain/kotlin/org/koin/core/resolution/CoreResolver.kt @@ -75,7 +75,7 @@ class CoreResolver( ?: resolveFromStackedParameters(scope,instanceContext) ?: resolveFromScopeSource(scope,instanceContext) ?: resolveFromScopeArchetype(scope,instanceContext) - ?: if (lookupParent) resolveFromParentScopes(scope,instanceContext) else null + ?: (if (lookupParent) resolveFromParentScopes(scope,instanceContext) else null) ?: resolveInExtensions(scope,instanceContext) } diff --git a/projects/gradle/libs.versions.toml b/projects/gradle/libs.versions.toml index f0cb0dd5e..c4e98507a 100644 --- a/projects/gradle/libs.versions.toml +++ b/projects/gradle/libs.versions.toml @@ -39,7 +39,7 @@ mockito = "4.8.0" mockk = "1.13.16" robolectric = "4.14.1" # Ktor -ktor = "3.1.3" +ktor = "3.2.1" slf4j = "2.0.17" uuidVersion = "0.8.4" @@ -73,7 +73,7 @@ androidx-startup = {module ="androidx.startup:startup-runtime", version.ref = "a # Ktor ktor-core = { module = "io.ktor:ktor-server-core", version.ref = "ktor" } -#ktor-core-di = { module = "io.ktor:ktor-server-di", version.ref = "ktor" } +ktor-core-di = { module = "io.ktor:ktor-server-di", version.ref = "ktor" } ktor-netty = { module = "io.ktor:ktor-server-netty", version.ref = "ktor" } ktor-testHost = { module = "io.ktor:ktor-server-test-host", version.ref = "ktor" } ktor-slf4j = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } diff --git a/projects/ktor/koin-ktor/build.gradle.kts b/projects/ktor/koin-ktor/build.gradle.kts index 3496ddb12..7d5ab50f5 100644 --- a/projects/ktor/koin-ktor/build.gradle.kts +++ b/projects/ktor/koin-ktor/build.gradle.kts @@ -41,8 +41,9 @@ kotlin { api(project(":core:koin-core")) // Ktor implementation(libs.ktor.core) - //TODO Ktor 3.2 -// implementation(libs.ktor.core.di) + implementation(libs.ktor.core.di) + // Coroutines + implementation(libs.kotlin.coroutines) } jvmTest.dependencies { implementation(libs.kotlin.test) diff --git a/projects/ktor/koin-ktor/src/commonMain/kotlin/org/koin/ktor/di/KoinDependencyMap.kt b/projects/ktor/koin-ktor/src/commonMain/kotlin/org/koin/ktor/di/KoinDependencyMap.kt index 7eabb2af8..1c286f0c0 100644 --- a/projects/ktor/koin-ktor/src/commonMain/kotlin/org/koin/ktor/di/KoinDependencyMap.kt +++ b/projects/ktor/koin-ktor/src/commonMain/kotlin/org/koin/ktor/di/KoinDependencyMap.kt @@ -1,26 +1,68 @@ +/* + * Copyright 2017-Present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.koin.ktor.di -//import io.ktor.server.plugins.di.DependencyKey -//import io.ktor.server.plugins.di.DependencyMap -//import org.koin.core.Koin -//import org.koin.core.qualifier.Qualifier -//import org.koin.core.qualifier.named -// -///** -// * Provide Ktor DI Support for Koin to allow reverse resolution from dependencies { } expression -// * -// * @param koin - Koin instance -// */ -//TODO Ktor 3.2 -//class KoinDependencyMap(val koin: Koin): DependencyMap { -// override fun contains(key: DependencyKey): Boolean { -// return key.qualifier !is Qualifier && koin.getOrNull(key.type.type, key.toQualifier()) == null -// } -// override fun get(key: DependencyKey): T { -// return koin.get(key.type.type, key.toQualifier()) -// } -//} -// -//private fun DependencyKey.toQualifier(): Qualifier? { -// return name?.let(::named) -//} \ No newline at end of file +import io.ktor.server.application.Application +import io.ktor.server.plugins.di.* +import org.koin.core.Koin +import org.koin.core.error.NoDefinitionFoundException +import org.koin.core.qualifier.Qualifier +import org.koin.core.qualifier.named +import org.koin.ktor.plugin.koin + +/** + * Full DependencyMapExtension integration for Koin with Ktor 3.2 DI + * + * This implementation provides seamless integration by implementing the + * DependencyMapExtension interface, allowing Ktor DI to automatically + * resolve dependencies from Koin when not found in Ktor's registry. + */ +class KoinDependencyMapExtension : DependencyMapExtension { + + override fun get(application: Application): DependencyMap { + return KoinDependencyMap(application.koin()) + } +} + +/** + * Koin implementation of DependencyMap interface + */ +class KoinDependencyMap(private val koin: Koin) : DependencyMap { + + override fun contains(key: DependencyKey): Boolean = + try { + resolve(key) + true + } catch (e: NoDefinitionFoundException) { + false + } + + override fun getInitializer(key: DependencyKey): DependencyInitializer = + DependencyInitializer.Explicit(key) { + resolve(key) + } + + // Here we are using Any because we do not have the type + private fun resolve(key: DependencyKey): Any { + val clazz = key.type.type + val qualifier = key.toQualifier() + return koin.get(clazz, qualifier) + } +} + +private fun DependencyKey.toQualifier(): Qualifier? { + return name?.let(::named) +} \ No newline at end of file diff --git a/projects/ktor/koin-ktor/src/commonMain/kotlin/org/koin/ktor/di/KtorDIExtension.kt b/projects/ktor/koin-ktor/src/commonMain/kotlin/org/koin/ktor/di/KtorDIExtension.kt index c0b9c8e14..0faae7e9d 100644 --- a/projects/ktor/koin-ktor/src/commonMain/kotlin/org/koin/ktor/di/KtorDIExtension.kt +++ b/projects/ktor/koin-ktor/src/commonMain/kotlin/org/koin/ktor/di/KtorDIExtension.kt @@ -15,25 +15,29 @@ */ package org.koin.ktor.di -//import io.ktor.server.application.Application -//import io.ktor.server.plugins.di.DependencyKey -//import io.ktor.server.plugins.di.dependencies -//import io.ktor.server.plugins.di.getOrNull -//import io.ktor.util.reflect.TypeInfo -//import org.koin.core.instance.ResolutionContext -//import org.koin.core.resolution.ResolutionExtension -//import org.koin.core.scope.Scope -// -///** -// * Ktor DI Resolver Extension to help Koin resolve Ktor DI objects -// * -// * @author Arnaud Giuliani -// */ -//TODO Ktor 3.2 - @see setupKoinApplication() -//internal class KtorDIExtension(private val application : Application) : ResolutionExtension { -// override val name: String = "ktor-di" -// override fun resolve(scope: Scope, instanceContext: ResolutionContext): Any? { -// val key = DependencyKey(TypeInfo(instanceContext.clazz), qualifier = instanceContext.qualifier?.value) -// return application.dependencies.getOrNull(key) -// } -//} \ No newline at end of file +import io.ktor.server.application.Application +import io.ktor.server.plugins.di.DependencyKey +import io.ktor.server.plugins.di.dependencies +import io.ktor.util.reflect.TypeInfo +import org.koin.core.instance.ResolutionContext +import org.koin.core.resolution.ResolutionExtension +import org.koin.core.scope.Scope +import kotlinx.coroutines.runBlocking + +/** + * Ktor DI Resolver Extension to help Koin resolve Ktor DI objects + * + * @author Arnaud Giuliani + */ +internal class KtorDIExtension(private val application : Application) : ResolutionExtension { + override val name: String = "ktor-di" + override fun resolve(scope: Scope, instanceContext: ResolutionContext): Any? { + val key = DependencyKey(TypeInfo(instanceContext.clazz), qualifier = instanceContext.qualifier?.value) + // runBlocking is required here because Ktor DI's get() function is suspend + // The blocking call is generally safe as dependency resolution is typically fast and non-blocking + // WARNING: This may cause problems for users as it can impact performance + return runBlocking { + application.dependencies.get(key) + } + } +} \ No newline at end of file diff --git a/projects/ktor/koin-ktor/src/commonMain/kotlin/org/koin/ktor/plugin/KoinPlugin.kt b/projects/ktor/koin-ktor/src/commonMain/kotlin/org/koin/ktor/plugin/KoinPlugin.kt index 74dc31788..e179c7088 100644 --- a/projects/ktor/koin-ktor/src/commonMain/kotlin/org/koin/ktor/plugin/KoinPlugin.kt +++ b/projects/ktor/koin-ktor/src/commonMain/kotlin/org/koin/ktor/plugin/KoinPlugin.kt @@ -34,6 +34,7 @@ import org.koin.core.module.Module import org.koin.core.scope.Scope import org.koin.dsl.KoinAppDeclaration import org.koin.dsl.ModuleDeclaration +import org.koin.ktor.di.KtorDIExtension import org.koin.mp.KoinPlatformTools /** @@ -58,8 +59,10 @@ val Koin = internal fun PluginBuilder.setupKoinApplication(): KoinApplication { val koinApplication = pluginConfig koinApplication.createEagerInstances() - //TODO Ktor 3.2 -// koinApplication.koin.resolver.addResolutionExtension(KtorDIExtension(application)) + + // Register KtorDIExtension for Ktor DI integration + koinApplication.koin.resolver.addResolutionExtension(KtorDIExtension(application)) + application.setKoinApplication(koinApplication) return koinApplication } diff --git a/projects/ktor/koin-ktor/src/commonMain/resources/META-INF/services/io.ktor.server.plugins.di.DependencyMapExtension b/projects/ktor/koin-ktor/src/commonMain/resources/META-INF/services/io.ktor.server.plugins.di.DependencyMapExtension new file mode 100644 index 000000000..d58c5e577 --- /dev/null +++ b/projects/ktor/koin-ktor/src/commonMain/resources/META-INF/services/io.ktor.server.plugins.di.DependencyMapExtension @@ -0,0 +1 @@ +org.koin.ktor.di.KoinDependencyMapExtension \ No newline at end of file diff --git a/projects/ktor/koin-ktor/src/jvmTest/kotlin/org/koin/ktor/di/KtorDIBridgeTest.kt b/projects/ktor/koin-ktor/src/jvmTest/kotlin/org/koin/ktor/di/KtorDIBridgeTest.kt new file mode 100644 index 000000000..9bab3b393 --- /dev/null +++ b/projects/ktor/koin-ktor/src/jvmTest/kotlin/org/koin/ktor/di/KtorDIBridgeTest.kt @@ -0,0 +1,128 @@ +package org.koin.ktor.di + +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.plugins.di.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import io.ktor.server.testing.* +import org.junit.Test +import org.koin.dsl.module +import org.koin.ktor.ext.inject +import org.koin.ktor.plugin.Koin +import kotlin.test.assertEquals + +class KtorDIBridgeTest { + + @Test + fun `should access Koin dependency via inject delegate`() = testApplication { + application { + bridgeModule() + } + + val response = client.get("/koin") + assertEquals(HttpStatusCode.OK, response.status) + assertEquals("Hello from Koin!", response.bodyAsText()) + } + + @Test + fun `should access Ktor DI dependency via dependencies delegate`() = testApplication { + application { + bridgeModule() + } + + val response = client.get("/ktor-di") + assertEquals(HttpStatusCode.OK, response.status) + assertEquals("Processed by Ktor DI", response.bodyAsText()) + } + + @Test + fun `should access both dependencies in mixed mode via dependencies delegate`() = testApplication { + application { + bridgeModule() + } + + val response = client.get("/mixed-ktor-di") + assertEquals(HttpStatusCode.OK, response.status) + assertEquals("Hello from Koin! - Processed by Ktor DI", response.bodyAsText()) + } + + @Test + fun `should access both dependencies in mixed mode via inject delegate`() = testApplication { + application { + bridgeModule() + } + + val response = client.get("/mixed-koin") + assertEquals(HttpStatusCode.OK, response.status) + assertEquals("Hello from Koin! - Processed by Ktor DI", response.bodyAsText()) + } + + private fun Application.bridgeModule() { + // Install Koin first + install(Koin) { + modules(module { + single { HelloServiceImpl() } + }) + } + + // Install Ktor DI and register dependencies + dependencies { + provide { KtorSpecificServiceImpl() } + } + + routing { + get("/koin") { + val helloService: HelloService by inject() // From koin + call.respond(helloService.sayHello()) + } + + get("/ktor-di") { + val ktorService: KtorSpecificService by dependencies // From Ktor DI + call.respond(ktorService.process()) + } + + get("/mixed-ktor-di") { + // Using both Koin and Ktor DI via dependencies delegate + val helloService: HelloService by dependencies // From Koin via Ktor DI + val ktorService: KtorSpecificService by dependencies // From Ktor DI + try { + call.respond("${helloService.sayHello()} - ${ktorService.process()}") + } catch (e: Exception) { + call.respond(HttpStatusCode.InternalServerError, "Error: ${e.message}") + } + + } + + get("/mixed-koin") { + // Using both Koin and Ktor DI via inject delegate + val helloService: HelloService by inject() // From Koin + val ktorService: KtorSpecificService by inject() // From Ktor DI via Koin + try { + call.respond("${helloService.sayHello()} - ${ktorService.process()}") + } catch (e: Exception) { + call.respond(HttpStatusCode.InternalServerError, "Error: ${e.message}") + } + } + } + } + +} + +interface HelloService { + fun sayHello(): String +} + +class HelloServiceImpl : HelloService { + override fun sayHello(): String = "Hello from Koin!" +} + +interface KtorSpecificService { + fun process(): String +} + +class KtorSpecificServiceImpl : KtorSpecificService { + override fun process(): String = "Processed by Ktor DI" +} \ No newline at end of file From 5c8c3421fa1a8d8b91b73820fa3c60b16931f0ed Mon Sep 17 00:00:00 2001 From: Lidonis Calhau Date: Wed, 23 Jul 2025 18:22:26 +0200 Subject: [PATCH 3/9] update ktor version for new release --- examples/gradle/versions.gradle | 2 +- projects/gradle/libs.versions.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/gradle/versions.gradle b/examples/gradle/versions.gradle index 24b9aa19f..e92aeb07a 100644 --- a/examples/gradle/versions.gradle +++ b/examples/gradle/versions.gradle @@ -7,7 +7,7 @@ ext { koin_compose_version = koin_version coroutines_version = "1.9.0" - ktor_version = "3.2.1" + ktor_version = "3.2.2" jb_compose_version = "1.8.0" // Test diff --git a/projects/gradle/libs.versions.toml b/projects/gradle/libs.versions.toml index c4e98507a..01078b4cc 100644 --- a/projects/gradle/libs.versions.toml +++ b/projects/gradle/libs.versions.toml @@ -39,7 +39,7 @@ mockito = "4.8.0" mockk = "1.13.16" robolectric = "4.14.1" # Ktor -ktor = "3.2.1" +ktor = "3.2.2" slf4j = "2.0.17" uuidVersion = "0.8.4" From 53cfc64ddb8621d2a74987e719a4c25e0b53c1ae Mon Sep 17 00:00:00 2001 From: Arnaud Giuliani Date: Wed, 3 Sep 2025 17:54:36 +0200 Subject: [PATCH 4/9] Open KoinApplication to allow add KoinKtorApplication DSL Bridge for Ktor DI. Explicit options for bridging Ktor -> Koin & Koin -> Ktor --- .../kotlin/org/koin/core/KoinApplication.kt | 2 +- .../org/koin/core/KoinKtorApplication.kt | 104 ++++++++++++++++++ .../org/koin/ktor/di/KoinDependencyMap.kt | 49 ++++++--- .../org/koin/ktor/di/KtorDIExtension.kt | 24 ++-- .../ktor/plugin/KoinIsolatedContextPlugin.kt | 4 +- .../kotlin/org/koin/ktor/plugin/KoinPlugin.kt | 26 +++-- ...r.server.plugins.di.DependencyMapExtension | 1 - .../org/koin/ktor/di/KtorDIBridgeTest.kt | 32 ++++-- 8 files changed, 193 insertions(+), 49 deletions(-) create mode 100644 projects/ktor/koin-ktor/src/commonMain/kotlin/org/koin/core/KoinKtorApplication.kt delete mode 100644 projects/ktor/koin-ktor/src/commonMain/resources/META-INF/services/io.ktor.server.plugins.di.DependencyMapExtension diff --git a/projects/core/koin-core/src/commonMain/kotlin/org/koin/core/KoinApplication.kt b/projects/core/koin-core/src/commonMain/kotlin/org/koin/core/KoinApplication.kt index bdf8851e2..b704e87fb 100644 --- a/projects/core/koin-core/src/commonMain/kotlin/org/koin/core/KoinApplication.kt +++ b/projects/core/koin-core/src/commonMain/kotlin/org/koin/core/KoinApplication.kt @@ -34,7 +34,7 @@ import kotlin.time.measureTime */ @OptIn(KoinInternalApi::class) @KoinApplicationDslMarker -class KoinApplication private constructor() { +open class KoinApplication protected constructor() { val koin = Koin() private var allowOverride = true diff --git a/projects/ktor/koin-ktor/src/commonMain/kotlin/org/koin/core/KoinKtorApplication.kt b/projects/ktor/koin-ktor/src/commonMain/kotlin/org/koin/core/KoinKtorApplication.kt new file mode 100644 index 000000000..4530ffdb3 --- /dev/null +++ b/projects/ktor/koin-ktor/src/commonMain/kotlin/org/koin/core/KoinKtorApplication.kt @@ -0,0 +1,104 @@ +/* + * Copyright 2017-Present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.koin.core + +import io.ktor.server.application.Application +import io.ktor.server.application.install +import io.ktor.server.plugins.di.DI +import org.koin.core.annotation.KoinExperimentalAPI +import org.koin.core.annotation.KoinInternalApi +import org.koin.core.module.KoinApplicationDslMarker +import org.koin.ktor.di.KoinDependencyMap +import org.koin.ktor.di.KtorDIExtension + +/** + * + */ +@OptIn(KoinInternalApi::class) +@KoinApplicationDslMarker +class KoinKtorApplication() : KoinApplication() { + + var ktorApplication : Application? = null + var ktorBridge : KtorBridgeDSL? = null + + /** + * Setup Bridge options for Koin & Ktor + * + * @ee KtorDSL + */ + @KoinExperimentalAPI + fun bridge(option : KtorBridgeDSL.() -> Unit){ + ktorBridge = KtorBridgeDSL() + ktorBridge!!.option() + } + + internal fun onPostStart(){ + ktorBridge?.let { ktorBridge -> + if (ktorBridge.bridgeKoinToKtor){ + onBridgeKoinToKtor() + } + if (ktorBridge.bridgeKtorToKoin){ + onBridgeKtorToKoin() + } + } + } + + internal fun onBridgeKoinToKtor(){ + koin.logger.debug("Ktor DI Bridge: Koin -> Ktor") + + koin.resolver.addResolutionExtension(KtorDIExtension(ktorApplication ?: error("KoinKtorApplication has no ktorApplication, when using koinToKtor()"))) + } + + internal fun onBridgeKtorToKoin(){ + koin.logger.debug("Ktor DI Bridge: Ktor -> Koin") + + val ktorApp = ktorApplication ?: error("KoinKtorApplication has no ktorApplication, when using ktorToKoin() ") + ktorApp.install(DI){ + include(KoinDependencyMap(koin)) + } + } + + companion object { + + fun init() : KoinKtorApplication { + return KoinKtorApplication() + } + } +} + +/** + * + */ +@OptIn(KoinInternalApi::class) +@KoinApplicationDslMarker +class KtorBridgeDSL { + + internal var bridgeKoinToKtor : Boolean = false + private set + + internal var bridgeKtorToKoin : Boolean = false + private set + + @KoinExperimentalAPI + fun koinToKtor() { + bridgeKoinToKtor = true + } + + @KoinExperimentalAPI + fun ktorToKoin() { + bridgeKtorToKoin = true + } +} \ No newline at end of file diff --git a/projects/ktor/koin-ktor/src/commonMain/kotlin/org/koin/ktor/di/KoinDependencyMap.kt b/projects/ktor/koin-ktor/src/commonMain/kotlin/org/koin/ktor/di/KoinDependencyMap.kt index 1c286f0c0..268a755a0 100644 --- a/projects/ktor/koin-ktor/src/commonMain/kotlin/org/koin/ktor/di/KoinDependencyMap.kt +++ b/projects/ktor/koin-ktor/src/commonMain/kotlin/org/koin/ktor/di/KoinDependencyMap.kt @@ -18,45 +18,60 @@ package org.koin.ktor.di import io.ktor.server.application.Application import io.ktor.server.plugins.di.* import org.koin.core.Koin +import org.koin.core.annotation.KoinInternalApi import org.koin.core.error.NoDefinitionFoundException import org.koin.core.qualifier.Qualifier import org.koin.core.qualifier.named import org.koin.ktor.plugin.koin -/** - * Full DependencyMapExtension integration for Koin with Ktor 3.2 DI - * - * This implementation provides seamless integration by implementing the - * DependencyMapExtension interface, allowing Ktor DI to automatically - * resolve dependencies from Koin when not found in Ktor's registry. - */ -class KoinDependencyMapExtension : DependencyMapExtension { - - override fun get(application: Application): DependencyMap { - return KoinDependencyMap(application.koin()) - } -} +///** +// * Full DependencyMapExtension integration for Koin with Ktor 3.2 DI +// * +// * This implementation provides seamless integration by implementing the +// * DependencyMapExtension interface, allowing Ktor DI to automatically +// * resolve dependencies from Koin when not found in Ktor's registry. +// */ +//class KoinDependencyMapExtension : DependencyMapExtension { +// +// override fun get(application: Application): DependencyMap { +// return KoinDependencyMap(application.koin()) +// } +//} /** * Koin implementation of DependencyMap interface */ +@OptIn(KoinInternalApi::class) class KoinDependencyMap(private val koin: Koin) : DependencyMap { - override fun contains(key: DependencyKey): Boolean = - try { + init { + println("[DEBUG] KoinDependencyMap init") + } + + val koinLogger = koin.logger + + override fun contains(key: DependencyKey): Boolean{ + koin.logger.debug("contains $key ?") + return try { resolve(key) true } catch (e: NoDefinitionFoundException) { false } + } - override fun getInitializer(key: DependencyKey): DependencyInitializer = - DependencyInitializer.Explicit(key) { + override fun getInitializer(key: DependencyKey): DependencyInitializer{ + koin.logger.debug("getInitializer $key ?") + + return DependencyInitializer.Explicit(key) { resolve(key) } + } // Here we are using Any because we do not have the type private fun resolve(key: DependencyKey): Any { + koin.logger.debug("resolve $key ?") + val clazz = key.type.type val qualifier = key.toQualifier() return koin.get(clazz, qualifier) diff --git a/projects/ktor/koin-ktor/src/commonMain/kotlin/org/koin/ktor/di/KtorDIExtension.kt b/projects/ktor/koin-ktor/src/commonMain/kotlin/org/koin/ktor/di/KtorDIExtension.kt index 0faae7e9d..e3754cefa 100644 --- a/projects/ktor/koin-ktor/src/commonMain/kotlin/org/koin/ktor/di/KtorDIExtension.kt +++ b/projects/ktor/koin-ktor/src/commonMain/kotlin/org/koin/ktor/di/KtorDIExtension.kt @@ -18,11 +18,11 @@ package org.koin.ktor.di import io.ktor.server.application.Application import io.ktor.server.plugins.di.DependencyKey import io.ktor.server.plugins.di.dependencies +import io.ktor.server.plugins.di.getBlocking import io.ktor.util.reflect.TypeInfo import org.koin.core.instance.ResolutionContext import org.koin.core.resolution.ResolutionExtension import org.koin.core.scope.Scope -import kotlinx.coroutines.runBlocking /** * Ktor DI Resolver Extension to help Koin resolve Ktor DI objects @@ -30,14 +30,24 @@ import kotlinx.coroutines.runBlocking * @author Arnaud Giuliani */ internal class KtorDIExtension(private val application : Application) : ResolutionExtension { + + init { + println("[DEBUG] KtorDIExtension init") + } + override val name: String = "ktor-di" + override fun resolve(scope: Scope, instanceContext: ResolutionContext): Any? { - val key = DependencyKey(TypeInfo(instanceContext.clazz), qualifier = instanceContext.qualifier?.value) - // runBlocking is required here because Ktor DI's get() function is suspend - // The blocking call is generally safe as dependency resolution is typically fast and non-blocking - // WARNING: This may cause problems for users as it can impact performance - return runBlocking { - application.dependencies.get(key) + val key = DependencyKey(TypeInfo(instanceContext.clazz), qualifier = instanceContext.qualifier?.value.toString()) + + println("[DEBUG] KtorDIExtension -> $key") + try { + val value = application.dependencies.getBlocking(key) + + println("[DEBUG] KtorDIExtension value? $value") + return value + } catch (e: Exception) { + error(e) } } } \ No newline at end of file diff --git a/projects/ktor/koin-ktor/src/commonMain/kotlin/org/koin/ktor/plugin/KoinIsolatedContextPlugin.kt b/projects/ktor/koin-ktor/src/commonMain/kotlin/org/koin/ktor/plugin/KoinIsolatedContextPlugin.kt index b0525fe7e..ab68dc66d 100644 --- a/projects/ktor/koin-ktor/src/commonMain/kotlin/org/koin/ktor/plugin/KoinIsolatedContextPlugin.kt +++ b/projects/ktor/koin-ktor/src/commonMain/kotlin/org/koin/ktor/plugin/KoinIsolatedContextPlugin.kt @@ -17,6 +17,7 @@ package org.koin.ktor.plugin import io.ktor.server.application.* import org.koin.core.KoinApplication +import org.koin.core.KoinKtorApplication import org.koin.core.annotation.KoinInternalApi /** @@ -26,9 +27,10 @@ import org.koin.core.annotation.KoinInternalApi * */ @OptIn(KoinInternalApi::class) -val KoinIsolated = createApplicationPlugin(name = "Koin", createConfiguration = { KoinApplication.init() }) { +val KoinIsolated = createApplicationPlugin(name = "Koin", createConfiguration = { KoinKtorApplication.init() }) { val koinApplication = setupKoinApplication() setupMonitoring(koinApplication) setupKoinScope(koinApplication) + koinApplication.ktorApplication = application koinApplication.koin.logger.info("Koin is using Ktor isolated context") } diff --git a/projects/ktor/koin-ktor/src/commonMain/kotlin/org/koin/ktor/plugin/KoinPlugin.kt b/projects/ktor/koin-ktor/src/commonMain/kotlin/org/koin/ktor/plugin/KoinPlugin.kt index e179c7088..951a40831 100644 --- a/projects/ktor/koin-ktor/src/commonMain/kotlin/org/koin/ktor/plugin/KoinPlugin.kt +++ b/projects/ktor/koin-ktor/src/commonMain/kotlin/org/koin/ktor/plugin/KoinPlugin.kt @@ -13,6 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@file:OptIn(KoinInternalApi::class) + package org.koin.ktor.plugin import io.ktor.server.application.Application @@ -27,6 +29,7 @@ import io.ktor.server.application.pluginOrNull import io.ktor.util.AttributeKey import org.koin.core.Koin import org.koin.core.KoinApplication +import org.koin.core.KoinKtorApplication import org.koin.core.annotation.KoinInternalApi import org.koin.core.context.startKoin import org.koin.core.context.stopKoin @@ -34,7 +37,6 @@ import org.koin.core.module.Module import org.koin.core.scope.Scope import org.koin.dsl.KoinAppDeclaration import org.koin.dsl.ModuleDeclaration -import org.koin.ktor.di.KtorDIExtension import org.koin.mp.KoinPlatformTools /** @@ -47,23 +49,27 @@ import org.koin.mp.KoinPlatformTools * */ val Koin = - createApplicationPlugin(name = "Koin", createConfiguration = { KoinApplication.init() }) { + createApplicationPlugin( + name = "Koin", + createConfiguration = { KoinKtorApplication.init() }) + { val koinApplication = setupKoinApplication() KoinPlatformTools.defaultContext().getOrNull()?.let { stopKoin() } // for ktor auto-reload startKoin(koinApplication) + + koinApplication.onPostStart() + setupMonitoring(koinApplication) setupKoinScope(koinApplication) } -@OptIn(KoinInternalApi::class) -internal fun PluginBuilder.setupKoinApplication(): KoinApplication { +internal fun PluginBuilder.setupKoinApplication(): KoinKtorApplication { val koinApplication = pluginConfig koinApplication.createEagerInstances() - - // Register KtorDIExtension for Ktor DI integration - koinApplication.koin.resolver.addResolutionExtension(KtorDIExtension(application)) - + + koinApplication.ktorApplication = application application.setKoinApplication(koinApplication) + return koinApplication } @@ -75,7 +81,7 @@ fun Application.setKoinApplication(koinApplication: KoinApplication) { attributes.put(KOIN_ATTRIBUTE_KEY, koinApplication.koin) } -internal fun PluginBuilder.setupMonitoring(koinApplication: KoinApplication) { +internal fun PluginBuilder.setupMonitoring(koinApplication: KoinKtorApplication) { val monitor = application.monitor monitor.raise(KoinApplicationStarted, koinApplication) monitor.subscribe(ApplicationStopping) { @@ -85,7 +91,7 @@ internal fun PluginBuilder.setupMonitoring(koinApplication: Koi } } -internal fun PluginBuilder.setupKoinScope(koinApplication: KoinApplication) { +internal fun PluginBuilder.setupKoinScope(koinApplication: KoinKtorApplication) { // Scope Handling on(CallSetup) { call -> val scopeComponent = RequestScope(koinApplication.koin, call) diff --git a/projects/ktor/koin-ktor/src/commonMain/resources/META-INF/services/io.ktor.server.plugins.di.DependencyMapExtension b/projects/ktor/koin-ktor/src/commonMain/resources/META-INF/services/io.ktor.server.plugins.di.DependencyMapExtension deleted file mode 100644 index d58c5e577..000000000 --- a/projects/ktor/koin-ktor/src/commonMain/resources/META-INF/services/io.ktor.server.plugins.di.DependencyMapExtension +++ /dev/null @@ -1 +0,0 @@ -org.koin.ktor.di.KoinDependencyMapExtension \ No newline at end of file diff --git a/projects/ktor/koin-ktor/src/jvmTest/kotlin/org/koin/ktor/di/KtorDIBridgeTest.kt b/projects/ktor/koin-ktor/src/jvmTest/kotlin/org/koin/ktor/di/KtorDIBridgeTest.kt index 9bab3b393..711f611a3 100644 --- a/projects/ktor/koin-ktor/src/jvmTest/kotlin/org/koin/ktor/di/KtorDIBridgeTest.kt +++ b/projects/ktor/koin-ktor/src/jvmTest/kotlin/org/koin/ktor/di/KtorDIBridgeTest.kt @@ -9,6 +9,7 @@ import io.ktor.server.response.* import io.ktor.server.routing.* import io.ktor.server.testing.* import org.junit.Test +import org.koin.core.logger.Level import org.koin.dsl.module import org.koin.ktor.ext.inject import org.koin.ktor.plugin.Koin @@ -41,7 +42,7 @@ class KtorDIBridgeTest { @Test fun `should access both dependencies in mixed mode via dependencies delegate`() = testApplication { application { - bridgeModule() + bridgeModule(ktorToKoin = true) } val response = client.get("/mixed-ktor-di") @@ -52,7 +53,7 @@ class KtorDIBridgeTest { @Test fun `should access both dependencies in mixed mode via inject delegate`() = testApplication { application { - bridgeModule() + bridgeModule(koinToKtor = true) } val response = client.get("/mixed-koin") @@ -60,12 +61,19 @@ class KtorDIBridgeTest { assertEquals("Hello from Koin! - Processed by Ktor DI", response.bodyAsText()) } - private fun Application.bridgeModule() { + private fun Application.bridgeModule(ktorToKoin : Boolean = false, koinToKtor : Boolean = false) { // Install Koin first install(Koin) { + printLogger(Level.DEBUG) + modules(module { - single { HelloServiceImpl() } + single { HelloServiceImpl() } }) + + bridge { + if (ktorToKoin) ktorToKoin() + if (koinToKtor) koinToKtor() + } } // Install Ktor DI and register dependencies @@ -75,8 +83,8 @@ class KtorDIBridgeTest { routing { get("/koin") { - val helloService: HelloService by inject() // From koin - call.respond(helloService.sayHello()) + val helloKoinService: HelloKoinService by inject() // From koin + call.respond(helloKoinService.sayHello()) } get("/ktor-di") { @@ -86,10 +94,10 @@ class KtorDIBridgeTest { get("/mixed-ktor-di") { // Using both Koin and Ktor DI via dependencies delegate - val helloService: HelloService by dependencies // From Koin via Ktor DI + val helloKoinService: HelloKoinService by dependencies // From Koin via Ktor DI val ktorService: KtorSpecificService by dependencies // From Ktor DI try { - call.respond("${helloService.sayHello()} - ${ktorService.process()}") + call.respond("${helloKoinService.sayHello()} - ${ktorService.process()}") } catch (e: Exception) { call.respond(HttpStatusCode.InternalServerError, "Error: ${e.message}") } @@ -98,10 +106,10 @@ class KtorDIBridgeTest { get("/mixed-koin") { // Using both Koin and Ktor DI via inject delegate - val helloService: HelloService by inject() // From Koin + val helloKoinService: HelloKoinService by inject() // From Koin val ktorService: KtorSpecificService by inject() // From Ktor DI via Koin try { - call.respond("${helloService.sayHello()} - ${ktorService.process()}") + call.respond("${helloKoinService.sayHello()} - ${ktorService.process()}") } catch (e: Exception) { call.respond(HttpStatusCode.InternalServerError, "Error: ${e.message}") } @@ -111,11 +119,11 @@ class KtorDIBridgeTest { } -interface HelloService { +interface HelloKoinService { fun sayHello(): String } -class HelloServiceImpl : HelloService { +class HelloServiceImpl : HelloKoinService { override fun sayHello(): String = "Hello from Koin!" } From 4cacef165cb7b8d609b12b2e3486bf76b37c438a Mon Sep 17 00:00:00 2001 From: Arnaud Giuliani Date: Wed, 3 Sep 2025 17:54:53 +0200 Subject: [PATCH 5/9] Update current sample --- examples/ktor-di-sample/build.gradle | 1 + .../org/koin/sample/ktor/di/Application.kt | 24 ++++++++++++------- .../org/koin/sample/ktor/di/Services.kt | 4 ++-- .../koin/sample/ktor/di/ApplicationTest.kt | 5 ++++ 4 files changed, 23 insertions(+), 11 deletions(-) diff --git a/examples/ktor-di-sample/build.gradle b/examples/ktor-di-sample/build.gradle index 34d03e919..9abd174b1 100644 --- a/examples/ktor-di-sample/build.gradle +++ b/examples/ktor-di-sample/build.gradle @@ -12,6 +12,7 @@ dependencies { implementation "io.ktor:ktor-server-di:$ktor_version" // Use local Koin project dependencies + implementation platform("io.insert-koin:koin-bom:$koin_version") implementation "io.insert-koin:koin-ktor" implementation "io.insert-koin:koin-logger-slf4j" implementation("org.slf4j:slf4j-api:2.0.16") diff --git a/examples/ktor-di-sample/src/main/kotlin/org/koin/sample/ktor/di/Application.kt b/examples/ktor-di-sample/src/main/kotlin/org/koin/sample/ktor/di/Application.kt index ea66f6eab..f835cf088 100644 --- a/examples/ktor-di-sample/src/main/kotlin/org/koin/sample/ktor/di/Application.kt +++ b/examples/ktor-di-sample/src/main/kotlin/org/koin/sample/ktor/di/Application.kt @@ -20,22 +20,28 @@ fun main() { fun Application.mainModule() { install(CallLogging) - + install(Koin) { printLogger(Level.DEBUG) + + bridge { + ktorToKoin() + koinToKtor() + } + modules(module { - single { HelloServiceImpl() } + single { HelloKoinServiceImpl() } }) } - + dependencies { provide { KtorSpecificServiceImpl() } } routing { get("/koin") { - val helloService: HelloService by inject() // From koin - call.respondText(helloService.sayHello()) + val helloKoinService: HelloKoinService by inject() // From koin + call.respondText(helloKoinService.sayHello()) } get("/ktor-di") { @@ -44,17 +50,17 @@ fun Application.mainModule() { } get("/mixed-ktor-di") { - val helloService: HelloService by dependencies // From Koin via Ktor DI + val helloKoinService: HelloKoinService by dependencies // From Koin via Ktor DI val ktorService: KtorSpecificService by dependencies // From Ktor DI - call.respondText("${helloService.sayHello()} - ${ktorService.process()}") + call.respondText("${helloKoinService.sayHello()} - ${ktorService.process()}") } get("/mixed-koin") { - val helloService: HelloService by inject() // From Koin + val helloKoinService: HelloKoinService by inject() // From Koin val ktorService: KtorSpecificService by inject() // From Ktor Di via Koin - call.respondText("${helloService.sayHello()} - ${ktorService.process()}") + call.respondText("${helloKoinService.sayHello()} - ${ktorService.process()}") } } } \ No newline at end of file diff --git a/examples/ktor-di-sample/src/main/kotlin/org/koin/sample/ktor/di/Services.kt b/examples/ktor-di-sample/src/main/kotlin/org/koin/sample/ktor/di/Services.kt index f0d69dae1..275d2cc4c 100644 --- a/examples/ktor-di-sample/src/main/kotlin/org/koin/sample/ktor/di/Services.kt +++ b/examples/ktor-di-sample/src/main/kotlin/org/koin/sample/ktor/di/Services.kt @@ -1,10 +1,10 @@ package org.koin.sample.ktor.di -interface HelloService { +interface HelloKoinService { fun sayHello(): String } -class HelloServiceImpl : HelloService { +class HelloKoinServiceImpl : HelloKoinService { override fun sayHello(): String = "Hello from Koin!" } diff --git a/examples/ktor-di-sample/src/test/kotlin/org/koin/sample/ktor/di/ApplicationTest.kt b/examples/ktor-di-sample/src/test/kotlin/org/koin/sample/ktor/di/ApplicationTest.kt index fab47717b..f6d00fe2f 100644 --- a/examples/ktor-di-sample/src/test/kotlin/org/koin/sample/ktor/di/ApplicationTest.kt +++ b/examples/ktor-di-sample/src/test/kotlin/org/koin/sample/ktor/di/ApplicationTest.kt @@ -5,6 +5,9 @@ import io.ktor.client.statement.bodyAsText import io.ktor.http.HttpStatusCode import io.ktor.server.testing.testApplication import org.junit.Test +import org.koin.core.annotation.KoinInternalApi +import org.koin.core.logger.Level +import org.koin.ktor.ext.getKoin import kotlin.test.assertEquals class ApplicationTest { @@ -15,6 +18,8 @@ class ApplicationTest { mainModule() } + @OptIn(KoinInternalApi::class) + val response = client.get("/koin") assertEquals(HttpStatusCode.Companion.OK, response.status) From 75d43066ddee65e78950a052e265fdac3fa7e73b Mon Sep 17 00:00:00 2001 From: Arnaud Giuliani Date: Wed, 3 Sep 2025 17:55:06 +0200 Subject: [PATCH 6/9] Uses latest Ktor --- projects/gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/projects/gradle/libs.versions.toml b/projects/gradle/libs.versions.toml index 01078b4cc..6c34556ef 100644 --- a/projects/gradle/libs.versions.toml +++ b/projects/gradle/libs.versions.toml @@ -3,7 +3,7 @@ # /!\ Koin in gradle.properties /!\ # Core -kotlin = "2.1.20" +kotlin = "2.1.21" binaryValidator = "0.16.3" publish = "2.0.0" coroutines = "1.10.2" @@ -39,7 +39,7 @@ mockito = "4.8.0" mockk = "1.13.16" robolectric = "4.14.1" # Ktor -ktor = "3.2.2" +ktor = "3.2.3" slf4j = "2.0.17" uuidVersion = "0.8.4" From 2527a16fddf68976784aee0bc2813fe985df7edd Mon Sep 17 00:00:00 2001 From: Arnaud Giuliani Date: Wed, 10 Sep 2025 16:55:41 +0200 Subject: [PATCH 7/9] 4.1.2-Beta1 --- examples/gradle/versions.gradle | 2 +- projects/gradle.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/gradle/versions.gradle b/examples/gradle/versions.gradle index bc08e7dbd..8c7dd8ab3 100644 --- a/examples/gradle/versions.gradle +++ b/examples/gradle/versions.gradle @@ -2,7 +2,7 @@ ext { // Kotlin kotlin_version = '2.1.20' // Koin Versions - koin_version = '4.1.1' + koin_version = '4.1.2-Beta1' koin_android_version = koin_version koin_compose_version = koin_version diff --git a/projects/gradle.properties b/projects/gradle.properties index 1c27cfb76..dd7cd3a7a 100644 --- a/projects/gradle.properties +++ b/projects/gradle.properties @@ -11,7 +11,7 @@ kotlin.incremental.multiplatform=true kotlin.code.style=official #Koin -koinVersion=4.1.1 +koinVersion=4.1.2-Beta1 #Compose org.jetbrains.compose.experimental.jscanvas.enabled=true From 2e2a54f314def15635708ab41a69ce801862c28b Mon Sep 17 00:00:00 2001 From: Arnaud Giuliani Date: Mon, 22 Sep 2025 18:15:16 +0200 Subject: [PATCH 8/9] introduce koin-dagger-bridge to help Koin inject components from Dagger side --- .../koin-dagger-bridge/build.gradle.kts | 47 ++++++++++++++ .../src/main/AndroidManifest.xml | 2 + .../org/koin/android/dagger/DaggerBridge.kt | 65 +++++++++++++++++++ projects/bom/koin-bom/build.gradle.kts | 1 + projects/gradle/libs.versions.toml | 3 + projects/settings.gradle.kts | 1 + 6 files changed, 119 insertions(+) create mode 100644 projects/android/koin-dagger-bridge/build.gradle.kts create mode 100644 projects/android/koin-dagger-bridge/src/main/AndroidManifest.xml create mode 100644 projects/android/koin-dagger-bridge/src/main/java/org/koin/android/dagger/DaggerBridge.kt diff --git a/projects/android/koin-dagger-bridge/build.gradle.kts b/projects/android/koin-dagger-bridge/build.gradle.kts new file mode 100644 index 000000000..884cc2b6b --- /dev/null +++ b/projects/android/koin-dagger-bridge/build.gradle.kts @@ -0,0 +1,47 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + alias(libs.plugins.androidLibrary) + alias(libs.plugins.kotlinAndroid) +} + +val androidCompileSDK : String by project +val androidMinSDK : String by project + +android { + namespace = "org.koin.android.dagger" + compileSdk = androidCompileSDK.toInt() + defaultConfig { + minSdk = androidMinSDK.toInt() + } + buildFeatures { + buildConfig = false + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + publishing { + singleVariant("release") {} + } +} + +tasks.withType().all { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_1_8) + } +} + +dependencies { + api(project(":android:koin-android")) + implementation(libs.dagger.core) +} + +// android sources +val sourcesJar: TaskProvider by tasks.registering(Jar::class) { + archiveClassifier.set("sources") + from(android.sourceSets.map { it.java.srcDirs }) +} + +apply(from = file("../../gradle/publish-android.gradle.kts")) diff --git a/projects/android/koin-dagger-bridge/src/main/AndroidManifest.xml b/projects/android/koin-dagger-bridge/src/main/AndroidManifest.xml new file mode 100644 index 000000000..8072ee00d --- /dev/null +++ b/projects/android/koin-dagger-bridge/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/projects/android/koin-dagger-bridge/src/main/java/org/koin/android/dagger/DaggerBridge.kt b/projects/android/koin-dagger-bridge/src/main/java/org/koin/android/dagger/DaggerBridge.kt new file mode 100644 index 000000000..f04072b70 --- /dev/null +++ b/projects/android/koin-dagger-bridge/src/main/java/org/koin/android/dagger/DaggerBridge.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.koin.android.dagger + +import dagger.hilt.EntryPoints +import org.koin.android.ext.koin.androidContext +import org.koin.core.scope.Scope +import kotlin.jvm.java +import kotlin.reflect.KClass + +/** + * Retrieve given T Module Entry Point from Dagger, to make it accessible to get a Dagger definition in Koin + * + * As Dagger Component example, to be injected in Koin : + * + * @InstallIn(SingletonComponent::class) + * @EntryPoint + * interface DaggerBridge { + * fun getUserDataRepository(): UserDataRepository + * } + * + * Bridge Example in Koin Annotations: + * + * @Module + * class AppModule { + * + * @Single + * fun bridgeUserDataRepository(scope : Scope) = scope.dagger().getUserDataRepository() + * } + * + * Bridge Example in Koin DSL: + * + * val appModule = module { + * single { dagger().getUserDataRepository() } + * } + * + * @param T - Reified type + */ +inline fun Scope.dagger() : T{ + return EntryPoints.get(androidContext().applicationContext, T::class.java) +} + +/** + * Retrieve given Module Entry Point from Dagger, to make it accessible to get a Dagger definition in Koin + * + * @param clazz - Target class type + * @see dagger() function doc + */ +fun Scope.dagger(clazz : KClass<*>) : T{ + return EntryPoints.get(androidContext().applicationContext, clazz.java) as T +} \ No newline at end of file diff --git a/projects/bom/koin-bom/build.gradle.kts b/projects/bom/koin-bom/build.gradle.kts index f00c15e77..349821a2b 100644 --- a/projects/bom/koin-bom/build.gradle.kts +++ b/projects/bom/koin-bom/build.gradle.kts @@ -28,6 +28,7 @@ dependencies { api(project(":android:koin-androidx-navigation")) api(project(":android:koin-androidx-workmanager")) api(project(":android:koin-androidx-startup")) + api(project(":android:koin-dagger-bridge")) api(project(":compose:koin-compose")) api(project(":compose:koin-compose-viewmodel")) diff --git a/projects/gradle/libs.versions.toml b/projects/gradle/libs.versions.toml index 094c74378..432df2748 100644 --- a/projects/gradle/libs.versions.toml +++ b/projects/gradle/libs.versions.toml @@ -18,6 +18,7 @@ android-activity = "1.10.1" android-fragment = "1.8.9" androidx-workmanager = "2.10.3" androidx-startup = "1.2.0" +dagger = "2.56" # Lifecycle androidx-lifecycle = "2.9.3" # Keep in sync with "jb-lifecycle" @@ -92,6 +93,8 @@ jb-composeNavigation = { module = "org.jetbrains.androidx.navigation:navigation- # Benchmark benchmark-runtime = {module = "org.jetbrains.kotlinx:kotlinx-benchmark-runtime", version.ref = "benchmark"} +dagger-core = { module = "com.google.dagger:hilt-core", version.ref = "dagger" } + [plugins] kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } kotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } diff --git a/projects/settings.gradle.kts b/projects/settings.gradle.kts index ad327f3fd..ba91a90f3 100644 --- a/projects/settings.gradle.kts +++ b/projects/settings.gradle.kts @@ -53,6 +53,7 @@ include( // Android ":android:koin-android", ":android:koin-android-compat", + ":android:koin-dagger-bridge", ":android:koin-androidx-navigation", ":android:koin-androidx-workmanager", ":android:koin-android-test", From 90edf60842dd72896468f19b7ac6059150b44418 Mon Sep 17 00:00:00 2001 From: Arnaud Giuliani Date: Mon, 20 Oct 2025 17:42:08 +0200 Subject: [PATCH 9/9] Fix Qualifier conversion --- .../src/commonMain/kotlin/org/koin/ktor/di/KtorDIExtension.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/ktor/koin-ktor/src/commonMain/kotlin/org/koin/ktor/di/KtorDIExtension.kt b/projects/ktor/koin-ktor/src/commonMain/kotlin/org/koin/ktor/di/KtorDIExtension.kt index e3754cefa..b35b68dbe 100644 --- a/projects/ktor/koin-ktor/src/commonMain/kotlin/org/koin/ktor/di/KtorDIExtension.kt +++ b/projects/ktor/koin-ktor/src/commonMain/kotlin/org/koin/ktor/di/KtorDIExtension.kt @@ -38,7 +38,7 @@ internal class KtorDIExtension(private val application : Application) : Resoluti override val name: String = "ktor-di" override fun resolve(scope: Scope, instanceContext: ResolutionContext): Any? { - val key = DependencyKey(TypeInfo(instanceContext.clazz), qualifier = instanceContext.qualifier?.value.toString()) + val key = DependencyKey(TypeInfo(instanceContext.clazz), qualifier = instanceContext.qualifier?.value) println("[DEBUG] KtorDIExtension -> $key") try {