Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Avoid retaining outdated LocalKoinApplication/LocalKoinScope #1586

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -45,10 +46,10 @@ inline fun <reified T : ViewModel> 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
)
}
}
Original file line number Diff line number Diff line change
@@ -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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The koin-compose module has already a KoinApplication function. Do we need a "special" one here?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this specific use-case, yes.

The KoinApplication function that already exists creates a new application and starts it. For this one, the assumption is that externally (likely in the Android Application class) an application was already created. This is an Android specific implementation because it uses the Android Context to find the relevant Koin application/scope. This is all not implemented for multiplatform Compose (as it works differently), so this code cannot live in koin-compose.

In short, this is needed to work around the issue that the composition local defaults are never changing between test runs, ensuring that we have a way of setting them to the correct value.

Personally I don't like this workaround, since we are introducing production code purely for test purposes, but with the way composition locals in Compose are built and the way Koin uses them (for composition-based scopes) I could not think of a good alternative.

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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -45,7 +46,7 @@ inline fun <reified T : ViewModel> 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)
Expand All @@ -60,10 +61,10 @@ inline fun <reified T : ViewModel> 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
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import org.koin.core.scope.Scope
@Composable
inline fun <reified T> koinInject(
qualifier: Qualifier? = null,
scope: Scope = LocalKoinScope.current,
scope: Scope = getKoinScope(),
noinline parameters: ParametersDefinition? = null,
): T = rememberKoinInject(qualifier, scope, parameters)

Expand All @@ -47,10 +47,8 @@ inline fun <reified T> koinInject(
@Composable
inline fun <reified T> rememberKoinInject(
qualifier: Qualifier? = null,
scope: Scope = LocalKoinScope.current,
scope: Scope = getKoinScope(),
noinline parameters: ParametersDefinition? = null,
): T = remember(qualifier, scope, parameters) {
scope.get(qualifier, parameters)
}


Original file line number Diff line number Diff line change
Expand Up @@ -19,28 +19,66 @@ 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

/**
* Current Koin Application context
*/
val LocalKoinApplication = compositionLocalOf { getKoinContext() }
val LocalKoinApplication = compositionLocalOf<Koin> { throw UnknownKoinContext() }

/**
* Current Koin Scope
*/
@OptIn(KoinInternalApi::class)
val LocalKoinScope = compositionLocalOf { getKoinContext().scopeRegistry.rootScope }
val LocalKoinScope = compositionLocalOf<Scope> { 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
Expand Down Expand Up @@ -84,4 +122,4 @@ fun KoinApplication(
) {
content()
}
}
}