-
-
Notifications
You must be signed in to change notification settings - Fork 727
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
Conversation
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 InsertKoinIOgh-1557
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 InsertKoinIOgh-1557
* @author Jan-Jelle Kester | ||
*/ | ||
@Composable | ||
fun KoinApplication(content: @Composable () -> Unit) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
@arnaudgiuliani Thanks for the heads-up, do to a busy time at work and the summer holidays I completely forgot about this. Your commit looks good at a first glance, I'll definitely replace the workaround in our project with this approach once released. I'd still would like to write a regression test for this, earlier I could not find a place in the codebase where to put it. Do you have any thoughts on this? I'd like to make the tests as "real" as possible by using the Koin test utilities on a dummy project, of course ensuring that the tests fail without applying |
@jjkester there is an issue on that: #1668 I believe I could make access to scope like this: fun currentKoinScope(): Scope = currentComposer.run {
try {
consume(LocalKoinScope)
} catch (_: UnknownKoinContext) {
val ctx = getKoinContext()
ctx.warnNoContext()
getKoinContext().scopeRegistry.rootScope
}
}
fun rememberCurrentKoinScope(): Scope = currentComposer.run {
remember {
try {
consume(LocalKoinScope)
} catch (_: UnknownKoinContext) {
val ctx = getKoinContext()
ctx.warnNoContext()
getKoinContext().scopeRegistry.rootScope
}
}
} With or without remember for LocalKoinScope, to help reevaluate scope if needed |
All the more reason to create some extensive test cases for this kind of thing (it is just fairly complicated!) Have not had time yet to delve into the mentioned issue, but from the surface it seems to make sense. Because the composition local is accessed from within Besides plenty of regression tests I'd also advise to check the performance without From a performance standpoint you'd want to avoid the In my project we did not run into this issue since we do not use |
I went with this PR #1723 I've tried manual benchmarking around the internals, with local measure calls. Seems to be faster without remember. |
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. |
The
CompositionLocal
s that Koin uses only work when the (GlobalContext
) Koin application is not changed at runtime, see #1557Option 1: The
KoinApplication
composableThe Koin application is looked up in the
Context
tree and the KoinCompositionLocal
s 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 thesetContent
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.Benefits:
Drawbacks:
Option 2: Throwing
UnknownKoinContext
in the default value factoryBy throwing an exception as default factory for the
CompositionLocal
s 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 functionsgetKoin()
andgetKoinScope()
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 pergetKoin()
call per composition.The default value of a
CompositionLocal
is singleton for the JVM process; when remembering a value as done in this approach the specific value is tied to the composition where it is used.Benefits:
KoinApplication
composable as compared to option 1Drawbacks:
Other options
Koin
andScope
objects, even when a different Koin application is started using the test ruleCompositionLocal
variant that evaluates the default value expression every time instead of usinglazy
(and wait a long time for them to implement it)Open tasks
Open questions to maintainer(s)