From 7bf2a846fbbed376f89223a2b6c9300776e60fa1 Mon Sep 17 00:00:00 2001 From: Jan-Jelle Kester Date: Tue, 16 May 2023 22:32:40 +0200 Subject: [PATCH 1/2] Inject current Koin application from Android context The Koin application is looked up in the Context tree and the Koin CompositionLocals are provided with/from this value. This (still) requires this function to be called around the first time Koin is used from Compose. A practical place for this is the setContent function. This means that (boilerplate) code needs to be added to every Activity or Fragment using Compose, and every test case where a Composable is tested in isolation. Untested - will need verification with these test frameworks! Fixes gh-1557 --- .../koin/androidx/compose/KoinApplication.kt | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 compose/koin-androidx-compose/src/main/java/org/koin/androidx/compose/KoinApplication.kt diff --git a/compose/koin-androidx-compose/src/main/java/org/koin/androidx/compose/KoinApplication.kt b/compose/koin-androidx-compose/src/main/java/org/koin/androidx/compose/KoinApplication.kt new file mode 100644 index 000000000..c6cee3666 --- /dev/null +++ b/compose/koin-androidx-compose/src/main/java/org/koin/androidx/compose/KoinApplication.kt @@ -0,0 +1,64 @@ +/* + * Copyright 2017-2023 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. + */ +@file:OptIn(KoinInternalApi::class) + +package org.koin.androidx.compose + +import android.app.Application +import android.content.ComponentCallbacks +import android.content.Context +import android.content.ContextWrapper +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import org.koin.android.ext.android.getKoin +import org.koin.compose.LocalKoinApplication +import org.koin.compose.LocalKoinScope +import org.koin.core.annotation.KoinInternalApi +import org.koin.core.component.KoinComponent + +/** + * Provide active Koin application from Android [Context] to Compose + * + * @param content - following compose function + * + * @author Jan-Jelle Kester + */ +@Composable +fun KoinApplication(content: @Composable () -> Unit) { + val context = LocalContext.current + val koinApplication = remember(context) { + context.findContextForKoin().getKoin() + } + CompositionLocalProvider( + LocalKoinApplication provides koinApplication, + LocalKoinScope provides koinApplication.scopeRegistry.rootScope, + content = content + ) +} + +/** + * Find the [KoinComponent] in the Context tree + */ +private fun Context.findContextForKoin(): ComponentCallbacks { + var context = this + while (context is ContextWrapper) { + if (context is KoinComponent && context is ComponentCallbacks) return context + context = context.baseContext + } + return applicationContext as Application +} From b784bf351a58666e9d2218d75b77b8d7a9e35e41 Mon Sep 17 00:00:00 2001 From: Jan-Jelle Kester Date: Wed, 17 May 2023 22:40:58 +0200 Subject: [PATCH 2/2] Look up LocalKoinApplication/LocalKoinScope if not explicitly provided By throwing an exception as default factory for the CompositionLocals we signal that there is no explicit value set. This will trigger the default lookup behavior that used to be the result of the factory function, as long as the appropriate functions getKoin() and getKoinScope() are used. By using the internal Compose API we are able to catch the exception to run the lookup code. We remember the result of the try/catch block to ensure we only incur the overhead of the exception once per `getKoin()` call per composition. Performance analysis and testing necessary! Fixes gh-1557 --- .../compose/navigation/NavViewModel.kt | 9 ++-- .../org/koin/androidx/compose/ViewModel.kt | 11 +++-- .../kotlin/org/koin/compose/Inject.kt | 6 +-- .../org/koin/compose/KoinApplication.kt | 48 +++++++++++++++++-- 4 files changed, 56 insertions(+), 18 deletions(-) diff --git a/compose/koin-androidx-compose-navigation/src/main/java/org/koin/androidx/compose/navigation/NavViewModel.kt b/compose/koin-androidx-compose-navigation/src/main/java/org/koin/androidx/compose/navigation/NavViewModel.kt index abaaeaefd..456ea67cf 100644 --- a/compose/koin-androidx-compose-navigation/src/main/java/org/koin/androidx/compose/navigation/NavViewModel.kt +++ b/compose/koin-androidx-compose-navigation/src/main/java/org/koin/androidx/compose/navigation/NavViewModel.kt @@ -18,11 +18,12 @@ package org.koin.androidx.compose.navigation import androidx.compose.runtime.Composable -import androidx.lifecycle.* +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelStoreOwner import androidx.lifecycle.viewmodel.CreationExtras import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner import org.koin.androidx.viewmodel.resolveViewModel -import org.koin.compose.LocalKoinScope +import org.koin.compose.getKoinScope import org.koin.core.annotation.KoinInternalApi import org.koin.core.parameter.ParametersDefinition import org.koin.core.qualifier.Qualifier @@ -45,10 +46,10 @@ inline fun koinNavViewModel( }, key: String? = null, extras: CreationExtras = defaultNavExtras(viewModelStoreOwner), - scope: Scope = LocalKoinScope.current, + scope: Scope = getKoinScope(), noinline parameters: ParametersDefinition? = null, ): T { return resolveViewModel( T::class, viewModelStoreOwner.viewModelStore, key, extras, qualifier, scope, parameters ) -} \ No newline at end of file +} diff --git a/compose/koin-androidx-compose/src/main/java/org/koin/androidx/compose/ViewModel.kt b/compose/koin-androidx-compose/src/main/java/org/koin/androidx/compose/ViewModel.kt index 8afc4e127..340cf8121 100644 --- a/compose/koin-androidx-compose/src/main/java/org/koin/androidx/compose/ViewModel.kt +++ b/compose/koin-androidx-compose/src/main/java/org/koin/androidx/compose/ViewModel.kt @@ -18,11 +18,12 @@ package org.koin.androidx.compose import androidx.compose.runtime.Composable -import androidx.lifecycle.* +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelStoreOwner import androidx.lifecycle.viewmodel.CreationExtras import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner import org.koin.androidx.viewmodel.resolveViewModel -import org.koin.compose.LocalKoinScope +import org.koin.compose.getKoinScope import org.koin.core.annotation.KoinInternalApi import org.koin.core.parameter.ParametersDefinition import org.koin.core.qualifier.Qualifier @@ -45,7 +46,7 @@ inline fun getViewModel( }, key: String? = null, extras: CreationExtras = defaultExtras(viewModelStoreOwner), - scope: Scope = LocalKoinScope.current, + scope: Scope = getKoinScope(), noinline parameters: ParametersDefinition? = null, ): T { return koinViewModel(qualifier, viewModelStoreOwner, key, extras, scope, parameters) @@ -60,10 +61,10 @@ inline fun koinViewModel( }, key: String? = null, extras: CreationExtras = defaultExtras(viewModelStoreOwner), - scope: Scope = LocalKoinScope.current, + scope: Scope = getKoinScope(), noinline parameters: ParametersDefinition? = null, ): T { return resolveViewModel( T::class, viewModelStoreOwner.viewModelStore, key, extras, qualifier, scope, parameters ) -} \ No newline at end of file +} diff --git a/compose/koin-compose/src/commonMain/kotlin/org/koin/compose/Inject.kt b/compose/koin-compose/src/commonMain/kotlin/org/koin/compose/Inject.kt index 7e210c065..ba4bc4149 100644 --- a/compose/koin-compose/src/commonMain/kotlin/org/koin/compose/Inject.kt +++ b/compose/koin-compose/src/commonMain/kotlin/org/koin/compose/Inject.kt @@ -33,7 +33,7 @@ import org.koin.core.scope.Scope @Composable inline fun koinInject( qualifier: Qualifier? = null, - scope: Scope = LocalKoinScope.current, + scope: Scope = getKoinScope(), noinline parameters: ParametersDefinition? = null, ): T = rememberKoinInject(qualifier, scope, parameters) @@ -47,10 +47,8 @@ inline fun koinInject( @Composable inline fun rememberKoinInject( qualifier: Qualifier? = null, - scope: Scope = LocalKoinScope.current, + scope: Scope = getKoinScope(), noinline parameters: ParametersDefinition? = null, ): T = remember(qualifier, scope, parameters) { scope.get(qualifier, parameters) } - - diff --git a/compose/koin-compose/src/commonMain/kotlin/org/koin/compose/KoinApplication.kt b/compose/koin-compose/src/commonMain/kotlin/org/koin/compose/KoinApplication.kt index bbd594175..6b689b22c 100644 --- a/compose/koin-compose/src/commonMain/kotlin/org/koin/compose/KoinApplication.kt +++ b/compose/koin-compose/src/commonMain/kotlin/org/koin/compose/KoinApplication.kt @@ -19,10 +19,14 @@ package org.koin.compose import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.InternalComposeApi import androidx.compose.runtime.compositionLocalOf +import androidx.compose.runtime.currentComposer +import androidx.compose.runtime.remember import org.koin.core.Koin import org.koin.core.annotation.KoinInternalApi import org.koin.core.module.Module +import org.koin.core.scope.Scope import org.koin.dsl.KoinAppDeclaration import org.koin.dsl.koinApplication import org.koin.mp.KoinPlatformTools @@ -30,17 +34,51 @@ import org.koin.mp.KoinPlatformTools /** * Current Koin Application context */ -val LocalKoinApplication = compositionLocalOf { getKoinContext() } +val LocalKoinApplication = compositionLocalOf { throw UnknownKoinContext() } /** * Current Koin Scope */ -@OptIn(KoinInternalApi::class) -val LocalKoinScope = compositionLocalOf { getKoinContext().scopeRegistry.rootScope } +val LocalKoinScope = compositionLocalOf { throw UnknownKoinContext() } + +/** + * Marker exception indicating that no Koin context is present in the composition + * + * @author Jan-Jelle Kester + */ +internal class UnknownKoinContext : RuntimeException("No Koin context has been provided") + private fun getKoinContext() = KoinPlatformTools.defaultContext().get() +/** + * Retrieve the current Koin application from the composition. + */ +@OptIn(InternalComposeApi::class) +@Composable +fun getKoin(): Koin = currentComposer.run { + remember { + try { + consume(LocalKoinApplication) + } catch (_: UnknownKoinContext) { + getKoinContext() + } + } +} + +/** + * Retrieve the current Koin scope from the composition + */ +@OptIn(InternalComposeApi::class) @Composable -fun getKoin(): Koin = LocalKoinApplication.current +fun getKoinScope(): Scope = currentComposer.run { + remember { + try { + consume(LocalKoinScope) + } catch (_: UnknownKoinContext) { + getKoinContext().scopeRegistry.rootScope + } + } +} /** * Start Koin Application from Compose @@ -84,4 +122,4 @@ fun KoinApplication( ) { content() } -} \ No newline at end of file +}