diff --git a/README.md b/README.md index 32ac51c..2725188 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ Stitch brings both models into one library without the combined trade-offs: | Runtime binding registration | ✅ | ❌ | ✅ | | Parent-child scope dependencies | ✅ | ✅ | ❌ | | Kotlin Multiplatform support | ✅ | ❌ | ✅ | +| Amount of rituals (boilerplate) | ✅ Lowest | ❌ Highest | ✅ Low | | Injection performance | ✅ Fastest | ✅ Fast | ❌ Slowest | | APK size impact on larger graphs | ✅ Lowest | ✅ Low | ❌ Highest | | Build-time impact on larger graphs | ✅ Lower than Dagger | ❌ Highest | ✅ Lowest | @@ -97,8 +98,6 @@ Resolve scoped bindings: ```kotlin val homeActivityScope = activityScope.createScope() -homeActivityScope.open() - val viewModel: HomeViewModel = homeActivityScope.get() // Cleanup @@ -112,8 +111,8 @@ Use `dependsOn()` to establish parent-child dependencies: val fragmentScope = scope("fragment").dependsOn(activityScope) // Use it anywhere -val homeActivityScope = activityScope.createScope().apply { open() } -val homeFragmentScope = homeActivityScope.createChildScope(fragmentScope).apply { open() } +val homeActivityScope = activityScope.createScope() +val homeFragmentScope = homeActivityScope.createChildScope(fragmentScope) val activityViewModel = homeFragmentScope.get() ``` diff --git a/stitch/api/stitch.api b/stitch/api/stitch.api index 90795c9..b3cc495 100644 --- a/stitch/api/stitch.api +++ b/stitch/api/stitch.api @@ -79,7 +79,6 @@ public final class com/harrytmthy/stitch/api/Scope { public final fun getParent ()Lcom/harrytmthy/stitch/api/Scope; public fun hashCode ()I public final fun isOpen ()Z - public final fun open ()V } public final class com/harrytmthy/stitch/api/ScopeKt { @@ -146,7 +145,7 @@ public final class com/harrytmthy/stitch/exception/MissingScopeException : com/h } public final class com/harrytmthy/stitch/exception/ScopeClosedException : com/harrytmthy/stitch/exception/GetFailedException { - public fun (Lkotlin/reflect/KClass;Lcom/harrytmthy/stitch/api/Qualifier;I)V + public fun (Lkotlin/reflect/KClass;Lcom/harrytmthy/stitch/api/Qualifier;Lcom/harrytmthy/stitch/api/Scope;)V } public final class com/harrytmthy/stitch/internal/DefinitionType : java/lang/Enum { diff --git a/stitch/src/commonMain/kotlin/com/harrytmthy/stitch/api/Component.kt b/stitch/src/commonMain/kotlin/com/harrytmthy/stitch/api/Component.kt index 10724aa..56f7a2f 100644 --- a/stitch/src/commonMain/kotlin/com/harrytmthy/stitch/api/Component.kt +++ b/stitch/src/commonMain/kotlin/com/harrytmthy/stitch/api/Component.kt @@ -143,7 +143,7 @@ class Component internal constructor() { private fun Scope.ensureOpen(type: KClass<*>, qualifier: Qualifier?) { if (!isOpen()) { - throw ScopeClosedException(type, qualifier, id) + throw ScopeClosedException(type, qualifier, this) } } } diff --git a/stitch/src/commonMain/kotlin/com/harrytmthy/stitch/api/Scope.kt b/stitch/src/commonMain/kotlin/com/harrytmthy/stitch/api/Scope.kt index 62b829e..b103d08 100644 --- a/stitch/src/commonMain/kotlin/com/harrytmthy/stitch/api/Scope.kt +++ b/stitch/src/commonMain/kotlin/com/harrytmthy/stitch/api/Scope.kt @@ -29,28 +29,17 @@ import kotlinx.atomicfu.atomic * ``` * val activityScope = scope("activity") * val instance = activityScope.createScope() - * instance.open() * * val viewModel: HomeViewModel = instance.get() * * instance.close() // Clears all cached instances for this scope * ``` * - * A scope must be opened before resolution and closed when no longer used. * Resolving from a closed scope throws [ScopeClosedException]. */ class Scope internal constructor(val id: Int, val name: String, val parent: Scope?) { - private val open = atomic(false) - - /** - * Opens this scope, allowing bindings to be resolved. - */ - fun open() { - val inner = ScopeManager.idsByScopeName.computeIfAbsent(name) { HashSet() } - inner.add(id) - open.value = true - } + private val open = atomic(true) /** * Closes this scope, clearing all cached instances and preventing further resolution. @@ -138,7 +127,12 @@ class ScopeRef(val name: String) { * * @param parent the parent scope instance, or null for a root scope. */ - fun createScope(parent: Scope? = null): Scope = Scope(id = ScopeManager.nextId(), name, parent) + fun createScope(parent: Scope? = null): Scope { + val scopeId = ScopeManager.nextId() + val inner = ScopeManager.idsByScopeName.computeIfAbsent(name) { HashSet() } + inner.add(scopeId) + return Scope(scopeId, name, parent) + } override fun hashCode(): Int = name.hashCode() diff --git a/stitch/src/commonMain/kotlin/com/harrytmthy/stitch/exception/ScopeClosedException.kt b/stitch/src/commonMain/kotlin/com/harrytmthy/stitch/exception/ScopeClosedException.kt index c9f6bdf..b221c3a 100644 --- a/stitch/src/commonMain/kotlin/com/harrytmthy/stitch/exception/ScopeClosedException.kt +++ b/stitch/src/commonMain/kotlin/com/harrytmthy/stitch/exception/ScopeClosedException.kt @@ -17,20 +17,19 @@ package com.harrytmthy.stitch.exception import com.harrytmthy.stitch.api.Qualifier +import com.harrytmthy.stitch.api.Scope import kotlin.reflect.KClass /** - * Thrown when trying to resolve a scoped binding using a scope that is not open. - * - * This guards against using instances after a lifecycle has ended, and against - * accessing scoped bindings before the scope is opened. - * - * To fix: - * - Call `scope.open()` before resolving. - * - Avoid holding on to references after `scope.close()`. + * Thrown when trying to resolve a scoped binding using a scope that is already closed. + * This guards against using instances after a lifecycle has ended. */ class ScopeClosedException( type: KClass<*>, qualifier: Qualifier?, - scopeId: Int, -) : GetFailedException(type, qualifier, explanation = "Scope with id '$scopeId' is not open!") + scope: Scope, +) : GetFailedException( + type = type, + qualifier = qualifier, + explanation = "Scope '${scope.name}' with id ${scope.id} is already closed!", +) diff --git a/stitch/src/commonTest/kotlin/com/harrytmthy/stitch/StitchTest.kt b/stitch/src/commonTest/kotlin/com/harrytmthy/stitch/StitchTest.kt index 0642884..b9e58ac 100644 --- a/stitch/src/commonTest/kotlin/com/harrytmthy/stitch/StitchTest.kt +++ b/stitch/src/commonTest/kotlin/com/harrytmthy/stitch/StitchTest.kt @@ -472,34 +472,6 @@ class StitchTest { } } - @Test - fun `scoped get with non open scope should throw ScopeClosedException`() { - val activityScope = scope("activity") - val module = module { - scoped(activityScope) { RepoImpl() as Repo } - } - Stitch.register(module) - - val scopeInstance = activityScope.createScope() // not opened - assertFailsWith { - Stitch.get(scope = scopeInstance) - } - } - - @Test - fun `open scope then get should work`() { - val activityScope = scope("activity") - val module = module { - scoped(activityScope) { RepoImpl() as Repo } - } - Stitch.register(module) - - val scopeInstance = activityScope.createScope() - scopeInstance.open() - val repo = Stitch.get(scope = scopeInstance) - assertNotNull(repo) - } - @Test fun `same scope instance returns same object`() { val screenScope = scope("screen") @@ -508,7 +480,7 @@ class StitchTest { } Stitch.register(module) - val scopeInstance = screenScope.createScope().apply { open() } + val scopeInstance = screenScope.createScope() val firstRepo = Stitch.get(scope = scopeInstance) val secondRepo = Stitch.get(scope = scopeInstance) assertSame(firstRepo, secondRepo) @@ -522,8 +494,8 @@ class StitchTest { } Stitch.register(module) - val firstScopeInstance = screenScope.createScope().apply { open() } - val secondScopeInstance = screenScope.createScope().apply { open() } + val firstScopeInstance = screenScope.createScope() + val secondScopeInstance = screenScope.createScope() val firstRepo = Stitch.get(scope = firstScopeInstance) val secondRepo = Stitch.get(scope = secondScopeInstance) @@ -538,7 +510,7 @@ class StitchTest { } Stitch.register(module) - val scopeInstance = screenScope.createScope().apply { open() } + val scopeInstance = screenScope.createScope() val repo = Stitch.get(scope = scopeInstance) assertNotNull(repo) @@ -548,29 +520,6 @@ class StitchTest { } } - @Test - fun `reopen same scope id rebuilds`() { - val screenScope = scope("screen") - var builds = 0 - val module = module { - scoped(screenScope) { - builds++ - RepoImpl() as Repo - } - } - Stitch.register(module) - - val scopeInstance = screenScope.createScope() - scopeInstance.open() - val first = Stitch.get(scope = scopeInstance) - scopeInstance.close() - - scopeInstance.open() - val second = Stitch.get(scope = scopeInstance) - assertNotSame(first, second) - assertEquals(2, builds) - } - @Test fun `wrong scope reference should throw IllegalStateException`() { val activityScope = scope("activity") @@ -580,7 +529,7 @@ class StitchTest { } Stitch.register(module) - val fragmentScopeInstance = fragmentScope.createScope().apply { open() } + val fragmentScopeInstance = fragmentScope.createScope() assertFailsWith { Stitch.get(scope = fragmentScopeInstance) } @@ -595,7 +544,7 @@ class StitchTest { } Stitch.register(module) - val scopeInstance = screenScope.createScope().apply { open() } + val scopeInstance = screenScope.createScope() val firstProd = Stitch.get(named("prod"), scope = scopeInstance) val secondProd = Stitch.get(named("prod"), scope = scopeInstance) val firstStaging = Stitch.get(named("staging"), scope = scopeInstance) @@ -614,7 +563,7 @@ class StitchTest { } Stitch.register(module) - val scopeInstance = screenScope.createScope().apply { open() } + val scopeInstance = screenScope.createScope() val asImpl = Stitch.get(scope = scopeInstance) val asRepo = Stitch.get(scope = scopeInstance) val asAuditable = Stitch.get(scope = scopeInstance) @@ -637,8 +586,8 @@ class StitchTest { } Stitch.register(module) - val activityScopeInstance = activityScope.createScope().apply { open() } - val viewModelScopeInstance = viewModelScope.createScope().apply { open() } + val activityScopeInstance = activityScope.createScope() + val viewModelScopeInstance = viewModelScope.createScope() val dao = Stitch.get(scope = activityScopeInstance) val logger = Stitch.get(scope = activityScopeInstance) val fetchUseCase = Stitch.get(scope = viewModelScopeInstance) @@ -660,55 +609,12 @@ class StitchTest { } Stitch.register(module) - val activityScopeInstance = activityScope.createScope().apply { open() } - val viewModelScopeInstance = viewModelScope.createScope().apply { open() } + val activityScopeInstance = activityScope.createScope() + val viewModelScopeInstance = viewModelScope.createScope() assertNotSame(Stitch.get(scope = activityScopeInstance), Stitch.get(scope = viewModelScopeInstance)) } - @Test - fun `scope should return different instance of the same type (with singleton)`() { - val activityScope = scope("activity") - val homeModule = module { - singleton { Logger() } - scoped(activityScope) { Logger() } - } - Stitch.register(homeModule) - val activityScopeInstance = activityScope.createScope().apply { open() } - - val singletonLogger = Stitch.get() - val scopedLogger = Stitch.get(scope = activityScopeInstance) - - assertSame(scopedLogger, activityScopeInstance.get()) - assertNotSame(singletonLogger, scopedLogger) - } - - @Test - fun `inject lazy throws when closed and succeeds when open`() { - val screenScope = scope("screen") - val module = module { - scoped(screenScope) { RepoImpl() as Repo } - } - Stitch.register(module) - - val scopeInstance = screenScope.createScope() - val lazyRepo: Lazy = scopeInstance.inject() - - // Access while closed → ScopeClosedException - assertFailsWith { lazyRepo.value } - - // Open and access → success - scopeInstance.open() - val repo = lazyRepo.value - assertNotNull(repo) - scopeInstance.close() - - // New Lazy should throw on access while closed - val newLazy: Lazy = scopeInstance.inject() - assertFailsWith { newLazy.value } - assertFailsWith { Stitch.get(scope = scopeInstance) } - } - @Test fun `unregister removes definitions even if scope is open`() { val screenScope = scope("screen") @@ -716,7 +622,7 @@ class StitchTest { scoped(screenScope) { RepoImpl() as Repo } } Stitch.register(module) - val scopeInstance = screenScope.createScope().apply { open() } + val scopeInstance = screenScope.createScope() assertNotNull(Stitch.get(scope = scopeInstance)) Stitch.unregisterAll() @@ -754,7 +660,7 @@ class StitchTest { } Stitch.register(module) - val scopeInstance = screenScope.createScope().apply { open() } + val scopeInstance = screenScope.createScope() val daoBefore = Stitch.get(scope = scopeInstance) val loggerBefore = Stitch.get(scope = scopeInstance) Stitch.unregister(module) @@ -812,8 +718,8 @@ class StitchTest { val viewModelModule = module { scoped(viewModelScope) { Logger() } } Stitch.register(activityModule, viewModelModule) - val activityScopeInstance = activityScope.createScope().apply { open() } - val viewModelScopeInstance = viewModelScope.createScope().apply { open() } + val activityScopeInstance = activityScope.createScope() + val viewModelScopeInstance = viewModelScope.createScope() assertNotNull(Stitch.get(scope = activityScopeInstance)) val loggerBeforeUnregister = Stitch.get(scope = viewModelScopeInstance) @@ -852,15 +758,15 @@ class StitchTest { scoped(activityScope) { Logger() } }.register() - val activityScopeInstance = activityScope.createScope().apply { open() } + val activityScopeInstance = activityScope.createScope() val activityLogger = activityScopeInstance.get() val fragmentScopeInstance = activityScopeInstance.createChildScope(fragmentScope) - .apply { open() } + val fragmentLogger = fragmentScopeInstance.get() assertSame(activityLogger, fragmentLogger) val anotherFragmentScopeInstance = fragmentScope.createScope(parent = activityScopeInstance) - .apply { open() } + val anotherFragmentLogger = anotherFragmentScopeInstance.get() assertSame(activityLogger, anotherFragmentLogger) } @@ -874,9 +780,9 @@ class StitchTest { scoped(fragmentScope) { Logger() } }.register() - val activityScopeInstance = activityScope.createScope().apply { open() } + val activityScopeInstance = activityScope.createScope() val activityLogger = activityScopeInstance.get() - val fragmentScopeInstance = fragmentScope.createScope().apply { open() } + val fragmentScopeInstance = fragmentScope.createScope() val fragmentLogger = fragmentScopeInstance.get() assertNotSame(activityLogger, fragmentLogger) } @@ -889,15 +795,15 @@ class StitchTest { scoped(activityScope) { ActivityLifecycleTracker() }.bind() }.register() - val activityScopeInstance = activityScope.createScope().apply { open() } + val activityScopeInstance = activityScope.createScope() val activityTracker = activityScopeInstance.get() val fragmentScopeInstance = activityScopeInstance.createChildScope(fragmentScope) - .apply { open() } + val fragmentTracker = fragmentScopeInstance.get() assertSame(activityTracker, fragmentTracker) val anotherFragmentScopeInstance = fragmentScope.createScope(parent = activityScopeInstance) - .apply { open() } + val anotherFragmentTracker = anotherFragmentScopeInstance.get() assertSame(activityTracker, anotherFragmentTracker) } @@ -910,7 +816,7 @@ class StitchTest { scoped(activityScope) { Logger() } }.register() - val fragmentScopeInstance = fragmentScope.createScope().apply { open() } + val fragmentScopeInstance = fragmentScope.createScope() assertFailsWith { fragmentScopeInstance.get() } } @@ -926,9 +832,8 @@ class StitchTest { scoped(fragmentScope) { C(get(), get()) } }.register() - val activityScopeInstance = activityScope.createScope().apply { open() } + val activityScopeInstance = activityScope.createScope() val fragmentScopeInstance = fragmentScope.createScope(activityScopeInstance) - .apply { open() } val fragmentScopedA = fragmentScopeInstance.get() val singletonScopedA = Stitch.get()