From e2a9215faa407d07b9193e72675445593cafcdb4 Mon Sep 17 00:00:00 2001 From: Arnaud Giuliani Date: Thu, 9 Jan 2025 19:17:39 +0100 Subject: [PATCH 1/7] Fixing ParametersHolder --- .../koin/core/parameter/ParametersHolder.kt | 9 ++-- .../org/koin/core/ParametersHolderTest.kt | 48 +++++++++++++++++++ .../kotlin/org/koin/core/ParametersTest.kt | 25 ++++++++++ 3 files changed, 78 insertions(+), 4 deletions(-) create mode 100644 projects/core/koin-core/src/jvmTest/kotlin/org/koin/core/ParametersTest.kt diff --git a/projects/core/koin-core/src/commonMain/kotlin/org/koin/core/parameter/ParametersHolder.kt b/projects/core/koin-core/src/commonMain/kotlin/org/koin/core/parameter/ParametersHolder.kt index 94cdd49ee..2819afcba 100644 --- a/projects/core/koin-core/src/commonMain/kotlin/org/koin/core/parameter/ParametersHolder.kt +++ b/projects/core/koin-core/src/commonMain/kotlin/org/koin/core/parameter/ParametersHolder.kt @@ -141,13 +141,14 @@ open class ParametersHolder( override fun toString(): String = "DefinitionParameters${_values.toList()}" override fun equals(other: Any?): Boolean { - if (other is ParametersHolder){ - return this.values == other.values - } else return false + if (this === other) return true + if (other !is ParametersHolder) return false + + return values == other.values && useIndexedValues == other.useIndexedValues } override fun hashCode(): Int { - return values.hashCode() + return 31 * values.hashCode() + (useIndexedValues?.hashCode() ?: 0) } } diff --git a/projects/core/koin-core/src/commonTest/kotlin/org/koin/core/ParametersHolderTest.kt b/projects/core/koin-core/src/commonTest/kotlin/org/koin/core/ParametersHolderTest.kt index 06aa53385..9ec04af94 100644 --- a/projects/core/koin-core/src/commonTest/kotlin/org/koin/core/ParametersHolderTest.kt +++ b/projects/core/koin-core/src/commonTest/kotlin/org/koin/core/ParametersHolderTest.kt @@ -134,4 +134,52 @@ class ParametersHolderTest { assertNotEquals(p1,p3) } + + @Test + fun `equality mutable check`() { + val p1 = parametersOf(1, 2, 3, 4) + val p2 = parametersOf(1, 2, 3, 4) + + assertEquals(p1, p2) + + p2.add(5) + + assertNotEquals(p1, p2) + } + + class DumBParam(v : ArrayList) : ParametersHolder(v) + + @Test + fun `equality check 2`() { + val p1 = DumBParam(arrayListOf(1, 2, 3, 4)) + val p2 = DumBParam(arrayListOf(1, 2, 3, 4)) + val p3 = DumBParam(arrayListOf(1, 2, 3)) + + assertEquals(p1, p2) + + assertNotEquals(p1,p3) + } + + @Test + fun `test equals considers useIndexedValues`() { + val holderWithIndexed = ParametersHolder(mutableListOf(1, 2, 3), useIndexedValues = true) + val holderWithoutIndexed = ParametersHolder(mutableListOf(1, 2, 3), useIndexedValues = false) + + // Assert they are not equal due to differing `useIndexedValues` + assertNotEquals(holderWithIndexed, holderWithoutIndexed, "ParametersHolder instances with different useIndexedValues should not be equal.") + } + + @Test + fun `test mutability affects hashCode and equality`() { + val holder = ParametersHolder(mutableListOf(1, 2, 3)) + val originalHashCode = holder.hashCode() + + // Modify the values list + holder.add(4) + // Assert hashCode changes after modification + assertNotEquals(originalHashCode, holder.hashCode(), "hashCode should reflect changes in the values list.") + // Assert equality is impacted + val holderUnmodified = ParametersHolder(mutableListOf(1, 2, 3)) + assertNotEquals(holder, holderUnmodified, "ParametersHolder should not be equal after its content is modified.") + } } diff --git a/projects/core/koin-core/src/jvmTest/kotlin/org/koin/core/ParametersTest.kt b/projects/core/koin-core/src/jvmTest/kotlin/org/koin/core/ParametersTest.kt new file mode 100644 index 000000000..23d0b713d --- /dev/null +++ b/projects/core/koin-core/src/jvmTest/kotlin/org/koin/core/ParametersTest.kt @@ -0,0 +1,25 @@ +package org.koin.core + +import org.koin.core.parameter.ParametersHolder +import kotlin.test.Test +import kotlin.test.assertEquals + +class ParametersTest { + + @Test + fun `test thread-safety in concurrent access`() { + val holder = ParametersHolder(mutableListOf(1, 2, 3)) + val threads = mutableListOf() + + for (i in 1..10) { + threads.add(Thread { + holder.add(i) + }) + } + + threads.forEach { it.start() } + threads.forEach { it.join() } + assertEquals(13, holder.size(), "ParametersHolder should contain all elements added concurrently.") + } + +} \ No newline at end of file From c2b6060e108f9c5d3483371df8d3a3373dee78b1 Mon Sep 17 00:00:00 2001 From: Arnaud Giuliani Date: Fri, 10 Jan 2025 15:04:31 +0100 Subject: [PATCH 2/7] 4.0.2-RC3 --- 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 08c022200..b1ac36a86 100644 --- a/examples/gradle/versions.gradle +++ b/examples/gradle/versions.gradle @@ -2,7 +2,7 @@ ext { // Kotlin kotlin_version = '2.0.21' // Koin Versions - koin_version = '4.0.2-RC2' + koin_version = '4.0.2-RC3' koin_android_version = koin_version koin_compose_version = koin_version diff --git a/projects/gradle.properties b/projects/gradle.properties index 2aaa79e5c..2fa5e5c78 100644 --- a/projects/gradle.properties +++ b/projects/gradle.properties @@ -8,7 +8,7 @@ org.gradle.parallel=true kotlin.code.style=official #Koin -koinVersion=4.0.2-RC2 +koinVersion=4.0.2-RC3 #Compose org.jetbrains.compose.experimental.jscanvas.enabled=true From f3c4f6f336646ebe7ab399a74569975cab388fcd Mon Sep 17 00:00:00 2001 From: Arnaud Giuliani Date: Fri, 10 Jan 2025 15:05:32 +0100 Subject: [PATCH 3/7] Adjust signature for koinInject and propose koinInject with ParametersHolder direct as API to avoid having to unwrap parameters --- .../kotlin/org/koin/compose/Inject.kt | 64 ++++++++++++++++--- 1 file changed, 56 insertions(+), 8 deletions(-) diff --git a/projects/compose/koin-compose/src/commonMain/kotlin/org/koin/compose/Inject.kt b/projects/compose/koin-compose/src/commonMain/kotlin/org/koin/compose/Inject.kt index a5bf9ff48..537fedc84 100644 --- a/projects/compose/koin-compose/src/commonMain/kotlin/org/koin/compose/Inject.kt +++ b/projects/compose/koin-compose/src/commonMain/kotlin/org/koin/compose/Inject.kt @@ -23,24 +23,72 @@ import org.koin.core.parameter.ParametersHolder import org.koin.core.qualifier.Qualifier import org.koin.core.scope.Scope + /** - * Resolve Koin dependency + * Resolve Koin dependency for given Type T + * + * Note this version unwrap parameters to ParametersHolder in order to let remember all parameters + * This parameters unwrap will be triggered on recomposition * - * @param qualifier - * @param scope - Koin's root default - * @param parameters - injected parameters + * For better performances we advise to use koinInject(Qualifier,Scope,ParametersHolder) + * + * @param qualifier - dependency qualifier + * @param scope - Koin's root by default + * @param parameters - injected parameters (with lambda & parametersOf()) + * @return instance of type T * * @author Arnaud Giuliani */ +@Composable @OptIn(KoinInternalApi::class) +inline fun koinInject( + qualifier: Qualifier? = null, + scope: Scope = currentKoinScope(), + noinline parameters: ParametersDefinition, +): T { + val p = parameters.invoke() + return remember(qualifier, scope, p) { + scope.getWithParameters(T::class, qualifier, p) + } +} + +/** + * Resolve Koin dependency for given Type T + * + * @param qualifier - dependency qualifier + * @param scope - Koin's root by default + * @param parameters - parameters (used with parametersOf(), no lambda) + * @return instance of type T + * + * @author Arnaud Giuliani + */ @Composable +@OptIn(KoinInternalApi::class) inline fun koinInject( qualifier: Qualifier? = null, scope: Scope = currentKoinScope(), - noinline parameters: ParametersDefinition? = null, + parameters: ParametersHolder, +): T { + return remember(qualifier, scope, parameters) { + scope.getWithParameters(T::class, qualifier, parameters) + } +} + +/** + * Resolve Koin dependency for given Type T + * + * @param qualifier - dependency qualifier + * @param scope - Koin's root by default + * @return instance of type T + * + * @author Arnaud Giuliani + */ +@Composable +inline fun koinInject( + qualifier: Qualifier? = null, + scope: Scope = currentKoinScope() ): T { - val params: ParametersHolder? = parameters?.invoke() - return remember(qualifier, scope, params) { - scope.getWithParameters(T::class, qualifier,params) + return remember(qualifier, scope) { + scope.get(T::class, qualifier) } } From a4abf5f8fd8cec6c386fa5041f3495d9be026217 Mon Sep 17 00:00:00 2001 From: Arnaud Giuliani Date: Fri, 10 Jan 2025 15:20:58 +0100 Subject: [PATCH 4/7] doc + test update test update --- docs/reference/koin-compose/compose.md | 58 ++++++++++++------- .../org/koin/sample/androidx/compose/App.kt | 46 +++++++++------ .../androidx/compose/MainApplication.kt | 1 - 3 files changed, 67 insertions(+), 38 deletions(-) diff --git a/docs/reference/koin-compose/compose.md b/docs/reference/koin-compose/compose.md index 7cf623b62..218e6d581 100644 --- a/docs/reference/koin-compose/compose.md +++ b/docs/reference/koin-compose/compose.md @@ -20,58 +20,59 @@ for an Android/Multiplatform app, use the following packages: - `koin-compose-viewmodel` - Compose ViewModel API - `koin-compose-viewmodel-navigation` - Compose ViewModel API with Navigation API integration -## Starting Koin in a Compose App with KoinApplication +## Starting over an existing Koin context (Koin already started) -The function `KoinApplication` helps to create Koin application instance, as a Composable: +Some time the `startKoin` function is already used in the application, to start Koin in your application (like in Android main app class, the Application class). In that case you need to inform your Compose application about the current Koin context with `KoinContext` or `KoinAndroidContext`. Those functions reuse current Koin context and bind it to the Compose application. ```kotlin @Composable fun App() { - KoinApplication(application = { - modules(...) - }) { - - // your screens here ... + // Set current Koin instance to Compose context + KoinContext() { + MyScreen() } } ``` -The `KoinApplication` function will handle start & stop of your Koin context, regarding the cycle of the Compose context. This function start and stop a new Koin application context. - :::info -In an Android Application, the `KoinApplication` will handle any need to stop/restart Koin context regarding configuration changes or drop of Activities. +Difference between `KoinAndroidContext` and `KoinContext`: +- `KoinAndroidContext` is looking into current Android app context for Koin instance +- `KoinContext` is looking into current GlobalContext for Koin instances ::: :::note -This replaces the use of the classic `startKoin` application function. +If you get some `ClosedScopeException` from a Composable, either use `KoinContext` on your Composable or ensure to have proper Koin start configuration [with Android context](/docs/reference/koin-android/start.md#from-your-application-class) ::: -## Starting over an existing Koin context +## Starting Koin with a Compose App - KoinApplication -Some time the `startKoin` function is already used in the application, to start Koin in your application (like in Android main app class, the Application class). In that case you need to inform your Compose application about the current Koin context with `KoinContext` or `KoinAndroidContext`. Those functions reuse current Koin context and bind it to the Compose application. +The function `KoinApplication` helps to create Koin application instance, as a Composable: ```kotlin @Composable fun App() { - // Set current Koin instance to Compose context - KoinContext() { - + KoinApplication(application = { + modules(...) + }) { + + // your screens here ... MyScreen() } } ``` +The `KoinApplication` function will handle start & stop of your Koin context, regarding the cycle of the Compose context. This function start and stop a new Koin application context. + :::info -Difference between `KoinAndroidContext` and `KoinContext`: -- `KoinAndroidContext` is looking into current Android app context for Koin instance -- `KoinContext` is looking into current GlobalContext for Koin instances +In an Android Application, the `KoinApplication` will handle any need to stop/restart Koin context regarding configuration changes or drop of Activities. ::: :::note -If you get some `ClosedScopeException` from a Composable, either use `KoinContext` on your Composable or ensure to have proper Koin start configuration [with Android context](/docs/reference/koin-android/start.md#from-your-application-class) +This replaces the use of the classic `startKoin` application function. ::: + ### Compose Preview with Koin The `KoinApplication` function is interesting to start dedicated context for preview. This can be also used to help with Compose preview: @@ -122,6 +123,23 @@ fun App(myService: MyService = koinInject()) { } ``` +### Injecting into a @Composable with Parameters + +While you request a new dependency from Koin, you may need to inject parameters. To do this you can use `parameters` parameter of the `koinInject` function, with the `parametersOf()` function like this: + +```kotlin +@Composable +fun App() { + val myService = koinInject(parameters = parametersOf("a_string")) +} +``` + +:::info +You can use parameters with lambda injection like `koinInject{ parametersOf("a_string") }`, but this can have a performance impact if your recomposing a lot around. This version with lambda needs to unwrap your parameters on call, to help avoid remembering your parameters. + +From version 4.0.2 of Koin, koinInject(Qualifier,Scope,ParametersHolder) is introduced to let you use parameters in the most efficient way +::: + ## ViewModel for @Composable The same way you have access to classical single/factory instances, you gain access to the following Koin ViewModel API: diff --git a/examples/sample-android-compose/src/main/java/org/koin/sample/androidx/compose/App.kt b/examples/sample-android-compose/src/main/java/org/koin/sample/androidx/compose/App.kt index e6f23085e..14a2b7307 100644 --- a/examples/sample-android-compose/src/main/java/org/koin/sample/androidx/compose/App.kt +++ b/examples/sample-android-compose/src/main/java/org/koin/sample/androidx/compose/App.kt @@ -3,8 +3,10 @@ package org.koin.sample.androidx.compose import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.Button import androidx.compose.material.Surface import androidx.compose.material.Text +import androidx.compose.material.TextField import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp @@ -20,6 +22,7 @@ import org.koin.sample.androidx.compose.data.MySingle import org.koin.sample.androidx.compose.di.secondModule import org.koin.sample.androidx.compose.viewmodel.SSHViewModel import org.koin.sample.androidx.compose.viewmodel.UserViewModel +import java.util.UUID @Composable @@ -43,6 +46,9 @@ fun App(userViewModel: UserViewModel = koinViewModel()) { item { ButtonForCreate("-X- Main") { created = !created } } + item { + MyScreen() + } } } else { Surface(modifier = Modifier.padding(8.dp)) { @@ -52,6 +58,27 @@ fun App(userViewModel: UserViewModel = koinViewModel()) { } +@Composable +fun MyScreen() { + + var someValue by remember { mutableStateOf("initial") } + val myDependency = koinInject(parameters = parametersOf(someValue)) + + SideEffect { + println("MyScreen 1") + } + + Column { + Text(text = myDependency.id) + TextField(someValue, onValueChange = { someValue = it }) + Button(onClick = { + if (someValue == "") someValue = "${UUID.randomUUID()}" + }) { + Text("Update") + } + } +} + @Composable fun ViewModelComposable( parentStatus: String = "- status -", @@ -70,21 +97,6 @@ fun ViewModelComposable( } } -// Preview - -//val fakeKoin = module { -// singleOf(::MySingle) -// factoryOf(::MyInnerFactory) -//} -// -//@Preview -//@Composable -//fun PreviewViewModelComposable() { -// KoinApplication(moduleList = { listOf(fakeKoin) }) { -// SingleComposable() -// } -//} - @Composable fun SingleComposable( @@ -105,7 +117,7 @@ fun SingleComposable( @Composable fun FactoryComposable( parentStatus: String = "- status -", - myFactory: MyFactory = koinInject { parametersOf("stable_status") } + myFactory: MyFactory = koinInject(parameters = parametersOf("stable_status")) ) { var created by remember { mutableStateOf(false) } rememberKoinModules(modules = { listOf(secondModule) }) @@ -132,7 +144,7 @@ fun FactoryComposable( //TODO Hold instance until recreate Composable fun InnerFactoryComposable( parentStatus: String, - myFactory: MyInnerFactory = koinInject { parametersOf("_stable_") } + myFactory: MyInnerFactory = koinInject(parameters = parametersOf("_stable_")) ) { var created by remember { mutableStateOf(false) } if (created) { diff --git a/examples/sample-android-compose/src/main/java/org/koin/sample/androidx/compose/MainApplication.kt b/examples/sample-android-compose/src/main/java/org/koin/sample/androidx/compose/MainApplication.kt index dc25fd002..71d5cab6b 100644 --- a/examples/sample-android-compose/src/main/java/org/koin/sample/androidx/compose/MainApplication.kt +++ b/examples/sample-android-compose/src/main/java/org/koin/sample/androidx/compose/MainApplication.kt @@ -14,7 +14,6 @@ class MainApplication : Application() { startKoin { androidLogger(Level.DEBUG) -// printLogger(Level.DEBUG) androidContext(this@MainApplication) modules(appModule) } From 73e5c6bc207312b29d7cb284d922faeeaefa4b37 Mon Sep 17 00:00:00 2001 From: Arnaud Giuliani Date: Mon, 13 Jan 2025 11:10:05 +0100 Subject: [PATCH 5/7] Fix doc for koinApplication @ Android start --- docs/reference/koin-android/start.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reference/koin-android/start.md b/docs/reference/koin-android/start.md index 64fdeabf5..5fd1c96aa 100644 --- a/docs/reference/koin-android/start.md +++ b/docs/reference/koin-android/start.md @@ -79,7 +79,7 @@ By using Gradle packge `koin-androidx-startup`, we can use `KoinStartup` interfa ```kotlin class MainApplication : Application(),KoinStartup { - override fun onKoinStartup(): KoinAppDeclaration = { + override fun onKoinStartup()= koinConfiguration { androidContext(this@MainApplication) modules(appModule) } @@ -90,7 +90,7 @@ class MainApplication : Application(),KoinStartup { } ``` -This replaces the `startKoin` function that is usally used in `onCreate`. +This replaces the `startKoin` function that is usally used in `onCreate`. The `koinConfiguration` function is returning a `KoinConfiguration` instance. :::info `KoinStartup` avoid blocking main thread at for startup time, and offers better performances. From 456a9a6fd40b82d74411f8e6308140d218ddbe18 Mon Sep 17 00:00:00 2001 From: Arnaud Giuliani Date: Mon, 13 Jan 2025 11:14:51 +0100 Subject: [PATCH 6/7] doc update --- docs/setup/koin.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/setup/koin.md b/docs/setup/koin.md index 089821717..ca46e1966 100644 --- a/docs/setup/koin.md +++ b/docs/setup/koin.md @@ -172,7 +172,7 @@ dependencies { ``` :::info -From now you can continue on Koin Tutorials to learn about using Koin: [Kotlin Multiplatform App Tutorial](/docs/quickstart/kmm) +From now you can continue on Koin Tutorials to learn about using Koin: [Kotlin Multiplatform App Tutorial](/docs/quickstart/kmp) ::: ### **Ktor** From 6329a4cc64d0b584f45ce21dec08361a2191e605 Mon Sep 17 00:00:00 2001 From: Arnaud Giuliani Date: Mon, 13 Jan 2025 17:35:17 +0100 Subject: [PATCH 7/7] gradle build cleanup --- projects/gradle.properties | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/projects/gradle.properties b/projects/gradle.properties index 2fa5e5c78..f019e6b9a 100644 --- a/projects/gradle.properties +++ b/projects/gradle.properties @@ -1,10 +1,13 @@ #Gradle -org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx2048M" +org.gradle.daemon=true +org.gradle.jvmargs=-Xmx4096M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx2048M" org.gradle.caching=true org.gradle.configuration-cache=true org.gradle.parallel=true #Kotlin +kotlin.incremental=true +kotlin.incremental.multiplatform=true kotlin.code.style=official #Koin @@ -18,5 +21,5 @@ org.jetbrains.compose.experimental.macos.enabled=true android.useAndroidX=true androidMinSDK=14 androidCompileSDK=34 -#android.nonTransitiveRClass=true +android.nonTransitiveRClass=true