Closed yschimke closed 1 month ago
I am experiencing the same issue with koin-androidx-compose:3.4.3. With koin-androidx-compose:3.4.1 + koin-android;3.4.0 everything works fine. The error I get is exactly the same:
Caused by org.koin.core.error.ClosedScopeException: Scope '_root_' is closed
at org.koin.core.scope.Scope.resolveInstance(Scope.kt:221)
at org.koin.core.scope.Scope.get(Scope.kt:210)
at org.koin.androidx.viewmodel.factory.KoinViewModelFactory.create(KoinViewModelFactory.kt:25)
at androidx.lifecycle.ViewModelProvider.get(ViewModelProvider.kt:187)
at androidx.lifecycle.ViewModelProvider.get(ViewModelProvider.kt:153)
at org.koin.androidx.viewmodel.GetViewModelKt.resolveViewModel(GetViewModel.kt:44)
I experienced this as well in an Android (connected) test. For me the first test that executes is OK, a subsequent one fails.
Did some debugging and the root cause seems to be the composition locals that Koin uses, LocalKoinApplication
and LocalKoinScope
.
The first time that these composition locals are read, the values are initialized. This means fetching the current Koin application from the default Koin context. All is fine.
Before starting the second test (using KoinTestRule
), a new Koin application is created. However, the composition locals are not initialized again. Hence, any Koin function in Compose is returning an application which has been closed.
Only retrieving the Koin application once is fine for production scenarios, but does not work well for tests.
I found the same workaround as described in the original issue, so that works for me (outside of Robolectric) as well.
Using:
io.insert-koin:koin-android:3.4.0
io.insert-koin:koin-androidx-compose:3.4.3
io.insert-koin:koin-test:3.4.0
io.insert-koin:koin-test-junit4:3.4.0
2023.01.00
We're experiencing this issue in production as well. In one scenario, we need to stop koin, start koin with updated modules and restart the main activity.
stopKoin()
startKoin { /* modules */ }
val componentName = ComponentName(context, MainActivity::class.java)
startActivity(Intent.makeRestartActivityTask(componentName))
When creating the new activity, we get the crash when first koinInject()
is called.
Caused by: org.koin.core.error.ClosedScopeException: Scope '_root_' is closed
at org.koin.core.scope.Scope.resolveInstance(Scope.kt:221)
at org.koin.core.scope.Scope.get(Scope.kt:210)
Workaround described in the original issue seems to be working for us too. We're using:
need to check 👌
@jjkester good catch:
Did some debugging and the root cause seems to be the composition locals that Koin uses, LocalKoinApplication and LocalKoinScope.
The first time that these composition locals are read, the values are initialized. This means fetching the current Koin application from the default Koin context. All is fine.
Before starting the second test (using KoinTestRule), a new Koin application is created. However, the composition locals are not initialized again. Hence, any Koin function in Compose is returning an application which has been closed.
I've introduced LocalKoinApplication
and LocalKoinScope
to help Compose idiomatic way to handle Scope & Application instance for Koin. Need to check how and why it's running like this with RobotElecltric
@arnaudgiuliani They run like that for instrumented tests on devices as well (we're not using Robolectric).
I do like the Compose API (koinInject
!) by the way, and it works brilliantly in production. However, during tests the lifecycles of things is a bit different. The lifecycle of the Koin application is for example shorter than the lifecycle of the Android application. My guesstimation is that is where the issue is.
As far as I have gathered we're running into some design decision in the compose runtime. The lambda function with which you provide the default value when defining the CompositionLocal
is only evaluated once, and Compose will keep the result of that evaluation for as long as possible. I have peeked at the source code of androidx.compose.runtime.CompositionLocal<T>
and the factory lambda is ending up in a Kotlin lazy
delegate. This is by the way also why reproviding (as in the workaround in the original post) works, because then Compose will actually reevaluate the expression to obtain the Koin context on every recomposition, and when it changes, invalidate everything that uses it so propagate that change.
For us the workaround works fine when testing a whole activity, because we only have to provide a value for the CompositionLocal
s once in the Activity's setContent
, but when testing Composables that use Koin all test cases would need wrapping which is not that nice. Of course there is extension functions to consider, but ideally it should just work.
Compose does offer the LocalContext
by default, maybe you can leverage that to rely on the Android Context
to retrieve the current KoinApplication
, since that should change between tests (at least, for Android instrumented tests).
I have had a look at the use cases that Koin has for these CompositionLocal
s. It is clear to me that they serve an important purpose, so refactoring them out does not seem feasible.
To take another direction, I have attempted to create a composable function which will look up the appropriate Koin application and provide it to the composition, based on the workaround we already have in our code and the workaround posted in the first post. It really needs to be tested that it covers all the use cases of Koin, and that it works for the different test frameworks.
The first one was not feasible for me to have a look at today (if at all - I will need some pointers!), and the second one would introduce a whole new class of tests to this repository (instrumented tests are not straightforward to run on a CI pipeline) so that does require some thinking (or input from @arnaudgiuliani) before attempting it in my opinion.
Code can be found in my fork: https://github.com/jjkester/koin/commit/7bf2a846fbbed376f89223a2b6c9300776e60fa1
yes interesting @jjkester the KoinApplication
composable function already exist in the common koin-compose
module, would require your findContextForKoin
function to be sure to catchup the right instance
Indeed, that function already exists but the difference being that the application already has been defined outside of Compose.
The context function has been based on this one from Accompanist, difference being that it is fine to fall back on the application context as that is the root one.
do you want to make a PR? else I can add it directly. That implies forcing to use KoinApplication
composable function. Still checking if it becomes necessary or not.
I'm happy to create a PR. I do have some more ideas though, because as I tried to explain earlier this may not cover all use cases (mainly testing standalone composables using ComposeContentTestRule
).
This solution does feel like a workaround, but I'm not confident there is a real, practical solution. (I'm also not confident that there is none at all...). So maybe a bit more investigation is warranted.
If I would be contributing the PR I do want to accompany it with a (regression) test suite to show that this indeed solves the issue. Not sure if it is sufficient to try and replicate the behaviour of the test frameworks, or whether it would be better to create actual tests using these actual frameworks. But as I mentioned before, we should consider where to put those and how to run them (especially the Android instrumented tests). Do you have any ideas about that?
yes, still wondering if it's a "robotelectric" case only of if this has to be part of the API design 🤔 I'm checking your PR
yes, still wondering if it's a "robotelectric" case only of if this has to be part of the API design 🤔 I'm checking your PR
@arnaudgiuliani, for us it's not only a robolectric case, see https://github.com/InsertKoinIO/koin/issues/1557#issuecomment-1512604377
I am not using Robolectric, so for sure not only an issue with that. Also with the Android Instrumented tests.
While running Android tests the application process stays alive, and the compositionLocal
is not re-initialized after the test rule created a new Koin application. Wondering whether this is the same on Compose multiplatform actually, but I have no experience with that. Might write a small test case in my PR.
ok great
Any news about this?
still on hold, need to follow up for next release 👍
ℹ️ We also have this crash on production. We let the user switch the language in-app and we stop/start Koin.
Our workaround is to restart the full process with ProcessPhoenix:
ProcessPhoenix.triggerRebirth(context)
koin-compose 1.1.0 & koin-androidx-compose 3.5.0 will bring KoinContext & KoinAndroidContext to setup compose with the right CompositionLocalProvider, depending on the current context (default context or android one)
61a88bbf79d593c9ed777f5b1acb07caa5e6db2e
Let's see how it goes with this.
We can replace your snippet with KoinAndroidContext
@yschimke
Soo, is this fixed?
I am trying to startKoin
in @Before fun
and stopKoin
in @After fun
on Koin 3.6.0-wasm-alpha2
. I am getting this exact exception: org.koin.core.error.ClosedScopeException: Scope '_root_' is closed
. I don't understand from this thread what should I do.
@mr-kew You need to wrap tests in either the KoinContext
or KoinAndroidContext
Composeables, so they can provide access to the correct KoinContext
@ben-gooding-sky Thanks for quick response. It's working brilliantly.
KoinAndroidContext
type does not exist for me though, but that may be because I'm on multiplatform?
Should I then wrap my composables inside setContent
in MainActivity
with KoinContext
too or would that cause problems? I am doing startKoin
inside onCreate
there.
@arnaudgiuliani
I was also having this issue, and was not using Roboeletric... same as @jjkester
Tried KoinContex and KoinAndroidContext wrappers in test without result...
so I dig into it and found a probable cause:
The Docs indicates this test rule:
class KoinTestRule(
private val modules: List<Module>
) : TestWatcher() {
override fun starting(description: Description) {
startKoin {
androidContext(InstrumentationRegistry.getInstrumentation().targetContext.applicationContext)
modules(modules)
}
}
override fun finished(description: Description) {
stopKoin()
}
}
This is making subsequent instrumented tests to fail
The fix is to use a rule like this:
class KoinTestRule(
private val modules: List<Module>
) : TestWatcher() {
override fun starting(description: Description) {
if (getKoinApplicationOrNull() == null) {
startKoin {
androidContext(InstrumentationRegistry.getInstrumentation().targetContext.applicationContext)
modules(modules)
}
} else {
loadKoinModules(modules)
}
}
override fun finished(description: Description) {
unloadKoinModules(modules)
}
}
regression spotted in https://github.com/InsertKoinIO/koin/issues/1900
Describe the bug
Using Koin with Robolectric, fails with
When it fails, these return different Koin instances in the composable
To Reproduce Steps to reproduce the behavior:
workaround
In main app composable, override
https://github.com/joreilly/Confetti/pull/580/files#diff-0548d0af1b708341bc3c142fbf71b1270fb90f0653f791f2eea9e92867d2e99f
Expected behavior Tests should pass as there is a fresh Koin instance for the current test
Koin project used and used version (please complete the following information): 3.4.0