diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1c4160bf..56a8c7fa 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -26,7 +26,7 @@ jetbrains-lifecycle = "2.8.0" hilt = '2.52' hilt-navigation-compose = '1.2.0' # Koin -koin = '3.5.6' +koin = '4.0.0-RC1' # Compose Multiplatform compose-plugin = "1.7.0-alpha02" # Test @@ -75,6 +75,8 @@ dagger-hilt-android-compiler = { module = 'com.google.dagger:hilt-android-compil dagger-hilt-android-testing = { module = 'com.google.dagger:hilt-android-testing', version.ref = 'hilt' } dagger-hilt-navigation-compose = { module = 'androidx.hilt:hilt-navigation-compose', version.ref = 'hilt-navigation-compose' } +koin-core = { module = 'io.insert-koin:koin-core', version.ref = 'koin' } +koin-compose = { module = 'io.insert-koin:koin-compose', version.ref = 'koin' } koin-android = { module = 'io.insert-koin:koin-android', version.ref = 'koin' } koin-android-test = { module = 'io.insert-koin:koin-android-test', version.ref = 'koin' } diff --git a/resacakoin/api/resacakoin.klib.api b/resacakoin/api/resacakoin.klib.api new file mode 100644 index 00000000..69561e3e --- /dev/null +++ b/resacakoin/api/resacakoin.klib.api @@ -0,0 +1,15 @@ +// Klib ABI Dump +// Targets: [iosArm64, iosSimulatorArm64, iosX64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: true +// - Show declarations: true + +// Library unique name: +final val com.sebaslogen.resaca.koin/com_sebaslogen_resaca_koin_KoinParametersHolder$stableprop // com.sebaslogen.resaca.koin/com_sebaslogen_resaca_koin_KoinParametersHolder$stableprop|#static{}com_sebaslogen_resaca_koin_KoinParametersHolder$stableprop[0] +final val com.sebaslogen.resaca.koin/com_sebaslogen_resaca_koin_KoinViewModelFactory$stableprop // com.sebaslogen.resaca.koin/com_sebaslogen_resaca_koin_KoinViewModelFactory$stableprop|#static{}com_sebaslogen_resaca_koin_KoinViewModelFactory$stableprop[0] + +final fun com.sebaslogen.resaca.koin/com_sebaslogen_resaca_koin_KoinParametersHolder$stableprop_getter(): kotlin/Int // com.sebaslogen.resaca.koin/com_sebaslogen_resaca_koin_KoinParametersHolder$stableprop_getter|com_sebaslogen_resaca_koin_KoinParametersHolder$stableprop_getter(){}[0] +final fun com.sebaslogen.resaca.koin/com_sebaslogen_resaca_koin_KoinViewModelFactory$stableprop_getter(): kotlin/Int // com.sebaslogen.resaca.koin/com_sebaslogen_resaca_koin_KoinViewModelFactory$stableprop_getter|com_sebaslogen_resaca_koin_KoinViewModelFactory$stableprop_getter(){}[0] +final inline fun <#A: reified androidx.lifecycle/ViewModel, #B: kotlin/Any> com.sebaslogen.resaca.koin/koinViewModelScoped(#B, noinline kotlin/Function1<#B, kotlin/Boolean>, org.koin.core.qualifier/Qualifier?, org.koin.core.scope/Scope?, noinline kotlin/Function0?, androidx.core.bundle/Bundle?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int): #A // com.sebaslogen.resaca.koin/koinViewModelScoped|koinViewModelScoped(0:1;kotlin.Function1<0:1,kotlin.Boolean>;org.koin.core.qualifier.Qualifier?;org.koin.core.scope.Scope?;kotlin.Function0?;androidx.core.bundle.Bundle?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){0§;1§}[0] +final inline fun <#A: reified androidx.lifecycle/ViewModel> com.sebaslogen.resaca.koin/koinViewModelScoped(kotlin/Any?, org.koin.core.qualifier/Qualifier?, org.koin.core.scope/Scope?, noinline kotlin/Function0?, androidx.core.bundle/Bundle?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int): #A // com.sebaslogen.resaca.koin/koinViewModelScoped|koinViewModelScoped(kotlin.Any?;org.koin.core.qualifier.Qualifier?;org.koin.core.scope.Scope?;kotlin.Function0?;androidx.core.bundle.Bundle?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){0§}[0] diff --git a/resacakoin/build.gradle.kts b/resacakoin/build.gradle.kts index 98a03101..b0d91735 100644 --- a/resacakoin/build.gradle.kts +++ b/resacakoin/build.gradle.kts @@ -1,10 +1,42 @@ +import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + plugins { + alias(libs.plugins.kotlin.multiplatform) alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) + alias(libs.plugins.jetbrains.compose) + alias(libs.plugins.compose.compiler) alias(libs.plugins.kover) alias(libs.plugins.dokka) alias(libs.plugins.maven) - alias(libs.plugins.compose.compiler) +} + +kotlin { + androidTarget { + @OptIn(ExperimentalKotlinGradlePluginApi::class) + compilerOptions { + jvmTarget.set(JvmTarget.JVM_17) + } + } + + listOf( + iosX64(), + iosArm64(), + iosSimulatorArm64() + ).forEach { iosTarget -> + iosTarget.binaries.framework { + baseName = "resacakoin" + isStatic = true + } + } + sourceSets { + commonMain.dependencies { + api(project(":resaca")) + + api(libs.koin.core) + api(libs.koin.compose) + } + } } android { @@ -28,9 +60,6 @@ android { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } - kotlinOptions { - jvmTarget = JavaVersion.VERSION_17.toString() - } packaging { resources { excludes += setOf( @@ -48,18 +77,6 @@ android { } } -dependencies { - - api(project(":resaca")) - - implementation(libs.androidx.core.ktx) - - implementation(libs.koin.android) - - // Integration with ViewModels - implementation(libs.androidx.lifecycle.viewmodel) -} - // Maven publishing configuration val artifactId = "resacakoin" val mavenGroup: String by project diff --git a/resacakoin/src/main/java/com/sebaslogen/resaca/koin/ScopedMemoizers.kt b/resacakoin/src/commonMain/kotlin/com/sebaslogen/resaca/koin/ScopedMemoizers.kt similarity index 74% rename from resacakoin/src/main/java/com/sebaslogen/resaca/koin/ScopedMemoizers.kt rename to resacakoin/src/commonMain/kotlin/com/sebaslogen/resaca/koin/ScopedMemoizers.kt index c8a9bcf5..7edf084d 100644 --- a/resacakoin/src/main/java/com/sebaslogen/resaca/koin/ScopedMemoizers.kt +++ b/resacakoin/src/commonMain/kotlin/com/sebaslogen/resaca/koin/ScopedMemoizers.kt @@ -1,11 +1,14 @@ package com.sebaslogen.resaca.koin -import android.os.Bundle import androidx.compose.runtime.Composable import androidx.compose.runtime.remember +import androidx.core.bundle.Bundle +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelStore +import androidx.lifecycle.createSavedStateHandle +import androidx.lifecycle.viewmodel.CreationExtras import com.sebaslogen.resaca.KeyInScopeResolver import com.sebaslogen.resaca.ScopeKeyWithResolver import com.sebaslogen.resaca.ScopedViewModelContainer @@ -13,12 +16,13 @@ import com.sebaslogen.resaca.ScopedViewModelContainer.ExternalKey import com.sebaslogen.resaca.ScopedViewModelContainer.InternalKey import com.sebaslogen.resaca.ScopedViewModelOwner import com.sebaslogen.resaca.generateKeysAndObserveLifecycle -import org.koin.androidx.viewmodel.factory.KoinViewModelFactory +import org.koin.compose.getKoin import org.koin.core.annotation.KoinInternalApi -import org.koin.core.context.GlobalContext import org.koin.core.parameter.ParametersDefinition +import org.koin.core.parameter.ParametersHolder import org.koin.core.qualifier.Qualifier import org.koin.core.scope.Scope +import kotlin.reflect.KClass /** @@ -50,9 +54,9 @@ public inline fun koinViewModelScoped( key: K, noinline keyInScopeResolver: KeyInScopeResolver, qualifier: Qualifier? = null, - scope: Scope = GlobalContext.get().scopeRegistry.rootScope, + scope: Scope = getKoin().scopeRegistry.rootScope, noinline parameters: ParametersDefinition? = null, - defaultArguments: Bundle = Bundle.EMPTY + defaultArguments: Bundle = Bundle() ): T { val scopeKeyWithResolver: ScopeKeyWithResolver = remember(key, keyInScopeResolver) { ScopeKeyWithResolver(key, keyInScopeResolver) } return koinViewModelScoped( @@ -90,9 +94,9 @@ public inline fun koinViewModelScoped( public inline fun koinViewModelScoped( key: Any? = null, qualifier: Qualifier? = null, - scope: Scope = GlobalContext.get().scopeRegistry.rootScope, + scope: Scope = getKoin().scopeRegistry.rootScope, noinline parameters: ParametersDefinition? = null, - defaultArguments: Bundle = Bundle.EMPTY + defaultArguments: Bundle = Bundle() ): T { val (scopedViewModelContainer: ScopedViewModelContainer, positionalMemoizationKey: InternalKey, externalKey: ExternalKey) = @@ -112,3 +116,49 @@ public inline fun koinViewModelScoped( defaultArguments = defaultArguments ) } + +/** + * Note: The two classes below are not yet KMP compatible in Koin 4.0.0-RC1, + * once Koin uses the KMP version of the AndroidX libraries, these classes will be removed. + * TODO: Remove these classes once Koin uses AndroidX KMP compatible libs + */ + +/** + * ViewModelProvider.Factory for Koin instances resolution + * @see ViewModelProvider.Factory + */ +@PublishedApi +internal class KoinViewModelFactory( + private val kClass: KClass, + private val scope: Scope, + private val qualifier: Qualifier? = null, + private val params: ParametersDefinition? = null +) : ViewModelProvider.Factory { + + override fun create(modelClass: KClass, extras: CreationExtras): T { + val koinParams = KoinParametersHolder(params, extras) + return scope.get(kClass, qualifier) { koinParams } + } +} + +@Suppress("UNCHECKED_CAST") +@PublishedApi +internal class KoinParametersHolder( + initialValues: ParametersDefinition? = null, + private val extras: CreationExtras, +) : ParametersHolder(initialValues?.invoke()?.values?.toMutableList() ?: mutableListOf()) { + + override fun elementAt(i: Int, clazz: KClass<*>): T { + return createSavedStateHandleOrElse(clazz) { super.elementAt(i, clazz) } + } + + override fun getOrNull(clazz: KClass<*>): T? { + return createSavedStateHandleOrElse(clazz) { super.getOrNull(clazz) } + } + + private fun createSavedStateHandleOrElse(clazz: KClass<*>, block: () -> T): T { + return if (clazz == SavedStateHandle::class) { + extras.createSavedStateHandle() as T + } else block() + } +} \ No newline at end of file diff --git a/resacakoin/src/main/AndroidManifest.xml b/resacakoin/src/main/AndroidManifest.xml deleted file mode 100644 index a5918e68..00000000 --- a/resacakoin/src/main/AndroidManifest.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/sample/src/main/java/com/sebaslogen/resacaapp/sample/di/koin/AppModule.kt b/sample/src/main/java/com/sebaslogen/resacaapp/sample/di/koin/AppModule.kt index 73b19cb8..81eda089 100644 --- a/sample/src/main/java/com/sebaslogen/resacaapp/sample/di/koin/AppModule.kt +++ b/sample/src/main/java/com/sebaslogen/resacaapp/sample/di/koin/AppModule.kt @@ -6,8 +6,8 @@ import com.sebaslogen.resacaapp.sample.ui.main.data.FakeScopedViewModel import com.sebaslogen.resacaapp.sample.ui.main.data.FakeSecondInjectedViewModel import com.sebaslogen.resacaapp.sample.ui.main.data.FakeSimpleInjectedViewModel import com.sebaslogen.resacaapp.sample.viewModelsClearedGloballySharedCounter -import org.koin.androidx.viewmodel.dsl.viewModel -import org.koin.androidx.viewmodel.dsl.viewModelOf +import org.koin.core.module.dsl.viewModel +import org.koin.core.module.dsl.viewModelOf import org.koin.dsl.module import java.util.concurrent.atomic.AtomicInteger diff --git a/sample/src/main/java/com/sebaslogen/resacaapp/sample/ui/main/ComposeActivity.kt b/sample/src/main/java/com/sebaslogen/resacaapp/sample/ui/main/ComposeActivity.kt index a5cd7a2d..3c38724b 100644 --- a/sample/src/main/java/com/sebaslogen/resacaapp/sample/ui/main/ComposeActivity.kt +++ b/sample/src/main/java/com/sebaslogen/resacaapp/sample/ui/main/ComposeActivity.kt @@ -34,6 +34,7 @@ import com.sebaslogen.resacaapp.sample.ui.main.compose.screens.ComposeScreenWith import com.sebaslogen.resacaapp.sample.ui.main.compose.screens.ComposeScreenWithSingleViewModelScopedWithKeys import com.sebaslogen.resacaapp.sample.ui.main.ui.theme.ResacaAppTheme import dagger.hilt.android.AndroidEntryPoint +import org.koin.compose.KoinContext const val emptyDestination = "emptyDestination" const val rememberScopedDestination = "rememberScopedDestination" @@ -103,7 +104,9 @@ fun ScreensWithNavigation(navController: NavHostController = rememberNavControll ComposeScreenWithHiltViewModelScoped(navController) } composable(koinViewModelScopedDestination) { - ComposeScreenWithKoinViewModelScoped(navController) + KoinContext { // This is required to use Koin in ActivityScenario Robolectric tests, see ComposeActivityRecreationTests.kt + ComposeScreenWithKoinViewModelScoped(navController) + } } composable(hiltSingleViewModelScopedDestination) { // This destination is only used in automated tests ComposeScreenWithSingleHiltViewModelScoped(navController) diff --git a/sample/src/test/java/com/sebaslogen/resacaapp/sample/koin/AssistedInjectionTest.kt b/sample/src/test/java/com/sebaslogen/resacaapp/sample/koin/AssistedInjectionTest.kt index ff77d005..a71c8ff3 100644 --- a/sample/src/test/java/com/sebaslogen/resacaapp/sample/koin/AssistedInjectionTest.kt +++ b/sample/src/test/java/com/sebaslogen/resacaapp/sample/koin/AssistedInjectionTest.kt @@ -14,6 +14,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import org.koin.compose.KoinContext import org.koin.core.parameter.parametersOf @@ -38,10 +39,12 @@ class AssistedInjectionTest : ComposeTestUtils { val fakeInjectedViewModelId = 555 var fakeInjectedViewModel: FakeInjectedViewModel? = null composeTestRule.activity.setContent { - fakeInjectedViewModel = koinViewModelScoped( - defaultArguments = bundleOf(FakeInjectedViewModel.MY_ARGS_KEY to fakeInjectedViewModelId), - parameters = { parametersOf(viewModelsClearedGloballySharedCounter) }) - DemoComposable(inputObject = fakeInjectedViewModel!!, objectType = "FakeInjectedViewModel", scoped = true) + KoinContext { + fakeInjectedViewModel = koinViewModelScoped( + defaultArguments = bundleOf(FakeInjectedViewModel.MY_ARGS_KEY to fakeInjectedViewModelId), + parameters = { parametersOf(viewModelsClearedGloballySharedCounter) }) + DemoComposable(inputObject = fakeInjectedViewModel!!, objectType = "FakeInjectedViewModel", scoped = true) + } } printComposeUiTreeToLog() diff --git a/sample/src/test/java/com/sebaslogen/resacaapp/sample/koin/ClearScopedViewModelTests.kt b/sample/src/test/java/com/sebaslogen/resacaapp/sample/koin/ClearScopedViewModelTests.kt index 9d840f9e..24c8d38f 100644 --- a/sample/src/test/java/com/sebaslogen/resacaapp/sample/koin/ClearScopedViewModelTests.kt +++ b/sample/src/test/java/com/sebaslogen/resacaapp/sample/koin/ClearScopedViewModelTests.kt @@ -25,6 +25,7 @@ import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import org.koin.compose.KoinContext @ExperimentalCoroutinesApi @RunWith(AndroidJUnit4::class) @@ -53,8 +54,10 @@ class ClearScopedViewModelTests : ComposeTestUtils { fun `when I navigate to nested screen and back, then the Koin scoped ViewModels of the second screen are cleared`() { // Given the starting screen with Koin injected ViewModel scoped composeTestRule.activity.setContent { - navController = rememberNavController() - ScreensWithNavigation(navController = navController, startDestination = koinViewModelScopedDestination) + KoinContext { + navController = rememberNavController() + ScreensWithNavigation(navController = navController, startDestination = koinViewModelScopedDestination) + } } printComposeUiTreeToLog() val initialAmountOfViewModelsCleared = viewModelsClearedGloballySharedCounter.get() @@ -84,10 +87,12 @@ class ClearScopedViewModelTests : ComposeTestUtils { var composablesShown by mutableStateOf(true) val textTitle = "Test text" composeTestRule.activity.setContent { - Column { - Text(textTitle) - if (composablesShown) { - DemoScopedKoinInjectedViewModelComposable() + KoinContext { + Column { + Text(textTitle) + if (composablesShown) { + DemoScopedKoinInjectedViewModelComposable() + } } } } @@ -117,11 +122,13 @@ class ClearScopedViewModelTests : ComposeTestUtils { var composablesShown by mutableStateOf(true) val textTitle = "Test text" composeTestRule.activity.setContent { - Column { - Text(textTitle) - DemoScopedKoinInjectedViewModelComposable() - if (composablesShown) { + KoinContext { + Column { + Text(textTitle) DemoScopedKoinInjectedViewModelComposable() + if (composablesShown) { + DemoScopedKoinInjectedViewModelComposable() + } } } } @@ -150,11 +157,13 @@ class ClearScopedViewModelTests : ComposeTestUtils { var composablesShown by mutableStateOf(true) val textTitle = "Test text" composeTestRule.activity.setContent { - Column { - Text(textTitle) - DemoScopedKoinInjectedViewModelComposable() - if (composablesShown) { - DemoScopedSecondKoinInjectedViewModelComposable() + KoinContext { + Column { + Text(textTitle) + DemoScopedKoinInjectedViewModelComposable() + if (composablesShown) { + DemoScopedSecondKoinInjectedViewModelComposable() + } } } } @@ -186,9 +195,11 @@ class ClearScopedViewModelTests : ComposeTestUtils { var viewModelKey by mutableStateOf("initial key") val textTitle = "Test text" composeTestRule.activity.setContent { - Column { - Text(textTitle) - DemoScopedKoinInjectedViewModelComposable(viewModelKey) + KoinContext { + Column { + Text(textTitle) + DemoScopedKoinInjectedViewModelComposable(viewModelKey) + } } } printComposeUiTreeToLog() diff --git a/sample/src/test/java/com/sebaslogen/resacaapp/sample/koin/KeyInScopeResolverTest.kt b/sample/src/test/java/com/sebaslogen/resacaapp/sample/koin/KeyInScopeResolverTest.kt index 1b3a59a8..18bbfc3a 100644 --- a/sample/src/test/java/com/sebaslogen/resacaapp/sample/koin/KeyInScopeResolverTest.kt +++ b/sample/src/test/java/com/sebaslogen/resacaapp/sample/koin/KeyInScopeResolverTest.kt @@ -35,6 +35,7 @@ import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import org.koin.compose.KoinContext import org.koin.core.parameter.parametersOf @@ -61,18 +62,20 @@ class KeyInScopeResolverTest : ComposeTestUtils { val listItems = (1..1000).toList().map { NumberContainer(it) } var height by mutableStateOf(1000.dp) composeTestRule.activity.setContent { - Box(modifier = Modifier.size(width = 200.dp, height = height)) { - val keys = rememberKeysInScope(inputListOfKeys = listItems) - LazyColumn(modifier = Modifier.fillMaxHeight()) { - items(items = listItems, key = { it.number }) { item -> - Box(modifier = Modifier.size(width = 200.dp, height = 100.dp)) { - val fakeScopedVM: FakeInjectedViewModel = - koinViewModelScoped( - key = item, - keyInScopeResolver = keys, - parameters = { parametersOf(viewModelsClearedGloballySharedCounter) } - ) - DemoComposable(inputObject = fakeScopedVM, objectType = "FakeInjectedViewModel $item", scoped = true) + KoinContext { + Box(modifier = Modifier.size(width = 200.dp, height = height)) { + val keys = rememberKeysInScope(inputListOfKeys = listItems) + LazyColumn(modifier = Modifier.fillMaxHeight()) { + items(items = listItems, key = { it.number }) { item -> + Box(modifier = Modifier.size(width = 200.dp, height = 100.dp)) { + val fakeScopedVM: FakeInjectedViewModel = + koinViewModelScoped( + key = item, + keyInScopeResolver = keys, + parameters = { parametersOf(viewModelsClearedGloballySharedCounter) } + ) + DemoComposable(inputObject = fakeScopedVM, objectType = "FakeInjectedViewModel $item", scoped = true) + } } } } @@ -103,19 +106,21 @@ class KeyInScopeResolverTest : ComposeTestUtils { // Given the starting screen with long lazy list of scoped objects remembering their keys val items: SnapshotStateList = (1..1000).toList().map { NumberContainer(it) }.toMutableStateList() composeTestRule.activity.setContent { - Box(modifier = Modifier.size(width = 200.dp, height = 1000.dp)) { - val listItems: SnapshotStateList = remember { items } - val keys = rememberKeysInScope(inputListOfKeys = listItems) - LazyColumn(modifier = Modifier.fillMaxHeight()) { - items(items = listItems, key = { it.number }) { item -> - Box(modifier = Modifier.size(width = 200.dp, height = 100.dp)) { - val fakeScopedVM: FakeInjectedViewModel = - koinViewModelScoped( - key = item, - keyInScopeResolver = keys, - parameters = { parametersOf(viewModelsClearedGloballySharedCounter) } - ) - DemoComposable(inputObject = fakeScopedVM, objectType = "FakeInjectedViewModel $item", scoped = true) + KoinContext { + Box(modifier = Modifier.size(width = 200.dp, height = 1000.dp)) { + val listItems: SnapshotStateList = remember { items } + val keys = rememberKeysInScope(inputListOfKeys = listItems) + LazyColumn(modifier = Modifier.fillMaxHeight()) { + items(items = listItems, key = { it.number }) { item -> + Box(modifier = Modifier.size(width = 200.dp, height = 100.dp)) { + val fakeScopedVM: FakeInjectedViewModel = + koinViewModelScoped( + key = item, + keyInScopeResolver = keys, + parameters = { parametersOf(viewModelsClearedGloballySharedCounter) } + ) + DemoComposable(inputObject = fakeScopedVM, objectType = "FakeInjectedViewModel $item", scoped = true) + } } } } @@ -149,21 +154,23 @@ class KeyInScopeResolverTest : ComposeTestUtils { var shown by mutableStateOf(true) val textTitle = "Test text" composeTestRule.activity.setContent { - Column { - Text(textTitle) - Box(modifier = Modifier.size(width = 200.dp, height = 1000.dp)) { - if (shown) { - val keys = rememberKeysInScope(inputListOfKeys = listItems) - LazyColumn(modifier = Modifier.fillMaxHeight()) { - items(items = listItems, key = { it.number }) { item -> - Box(modifier = Modifier.size(width = 200.dp, height = 100.dp)) { - val fakeScopedVM: FakeInjectedViewModel = - koinViewModelScoped( - key = item, - keyInScopeResolver = keys, - parameters = { parametersOf(viewModelsClearedGloballySharedCounter) } - ) - DemoComposable(inputObject = fakeScopedVM, objectType = "FakeInjectedViewModel $item", scoped = true) + KoinContext { + Column { + Text(textTitle) + Box(modifier = Modifier.size(width = 200.dp, height = 1000.dp)) { + if (shown) { + val keys = rememberKeysInScope(inputListOfKeys = listItems) + LazyColumn(modifier = Modifier.fillMaxHeight()) { + items(items = listItems, key = { it.number }) { item -> + Box(modifier = Modifier.size(width = 200.dp, height = 100.dp)) { + val fakeScopedVM: FakeInjectedViewModel = + koinViewModelScoped( + key = item, + keyInScopeResolver = keys, + parameters = { parametersOf(viewModelsClearedGloballySharedCounter) } + ) + DemoComposable(inputObject = fakeScopedVM, objectType = "FakeInjectedViewModel $item", scoped = true) + } } } } diff --git a/sample/src/test/java/com/sebaslogen/resacaapp/sample/koin/ScopeKeysTest.kt b/sample/src/test/java/com/sebaslogen/resacaapp/sample/koin/ScopeKeysTest.kt index 73b4fdb2..01e53e1a 100644 --- a/sample/src/test/java/com/sebaslogen/resacaapp/sample/koin/ScopeKeysTest.kt +++ b/sample/src/test/java/com/sebaslogen/resacaapp/sample/koin/ScopeKeysTest.kt @@ -26,6 +26,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import org.koin.compose.KoinContext import org.koin.core.parameter.parametersOf @@ -48,17 +49,19 @@ class ScopeKeysTest : ComposeTestUtils { // Given the starting screen with scoped object that uses a key composeTestRule.activity.setContent { - var myKey by remember { mutableStateOf(false) } - val fakeInjectedViewModel: FakeInjectedViewModel = - koinViewModelScoped( - key = myKey, - defaultArguments = bundleOf(FakeInjectedViewModel.MY_ARGS_KEY to 123), - parameters = { parametersOf(viewModelsClearedGloballySharedCounter) } - ) - DemoComposable(inputObject = fakeInjectedViewModel, objectType = "FakeInjectedViewModel", scoped = true) - Button(modifier = Modifier.testTag("Button"), - onClick = { myKey = !myKey }) { - Text("Click to change") + KoinContext { + var myKey by remember { mutableStateOf(false) } + val fakeInjectedViewModel: FakeInjectedViewModel = + koinViewModelScoped( + key = myKey, + defaultArguments = bundleOf(FakeInjectedViewModel.MY_ARGS_KEY to 123), + parameters = { parametersOf(viewModelsClearedGloballySharedCounter) } + ) + DemoComposable(inputObject = fakeInjectedViewModel, objectType = "FakeInjectedViewModel", scoped = true) + Button(modifier = Modifier.testTag("Button"), + onClick = { myKey = !myKey }) { + Text("Click to change") + } } } printComposeUiTreeToLog() diff --git a/sample/src/test/java/com/sebaslogen/resacaapp/sample/utils/ComposeTestUtils.kt b/sample/src/test/java/com/sebaslogen/resacaapp/sample/utils/ComposeTestUtils.kt index 169b80cf..99d06379 100644 --- a/sample/src/test/java/com/sebaslogen/resacaapp/sample/utils/ComposeTestUtils.kt +++ b/sample/src/test/java/com/sebaslogen/resacaapp/sample/utils/ComposeTestUtils.kt @@ -40,7 +40,7 @@ interface ComposeTestUtils { fun tearDown() { stopKoin() showSingleScopedViewModel = null - viewModelsClearedGloballySharedCounter.setPlain(0) + viewModelsClearedGloballySharedCounter.plain = 0 } // Helper functions //