diff --git a/composeApp/src/commonMain/kotlin/data/di/DataModule.kt b/composeApp/src/commonMain/kotlin/data/di/DataModule.kt index 5a695489..a0dd5180 100644 --- a/composeApp/src/commonMain/kotlin/data/di/DataModule.kt +++ b/composeApp/src/commonMain/kotlin/data/di/DataModule.kt @@ -5,9 +5,8 @@ import data.LessonRepository import data.TopicsRepository import ivy.di.Di import ivy.di.Di.register -import ivy.di.DiModule -object DataModule : DiModule { +object DataModule : Di.Module { override fun init() = Di.appScope { register { TopicsRepository(Di.get(), Di.get()) } register { CourseRepository(Di.get(), Di.get()) } diff --git a/composeApp/src/commonMain/kotlin/di/AppModule.kt b/composeApp/src/commonMain/kotlin/di/AppModule.kt index 5990ee28..00763a24 100644 --- a/composeApp/src/commonMain/kotlin/di/AppModule.kt +++ b/composeApp/src/commonMain/kotlin/di/AppModule.kt @@ -3,13 +3,12 @@ package di import ivy.di.Di import ivy.di.Di.register import ivy.di.Di.singleton -import ivy.di.DiModule import systemNavigation import ui.navigation.Navigation import util.DispatchersProvider import util.DispatchersProviderImpl -object AppModule : DiModule { +object AppModule : Di.Module { override fun init() { Di.appScope { diff --git a/composeApp/src/commonMain/kotlin/ui/navigation/Screen.kt b/composeApp/src/commonMain/kotlin/ui/navigation/Screen.kt index 1d7b931a..a76a2eca 100644 --- a/composeApp/src/commonMain/kotlin/ui/navigation/Screen.kt +++ b/composeApp/src/commonMain/kotlin/ui/navigation/Screen.kt @@ -2,6 +2,7 @@ package ui.navigation import androidx.compose.runtime.Composable import ivy.di.Di +import ivy.di.FeatureScope import kotlinx.coroutines.CompletableJob import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -14,17 +15,17 @@ abstract class Screen { private lateinit var job: CompletableJob protected lateinit var screenScope: CoroutineScope - protected abstract fun onDi(): Di.ScreenScope.() -> Unit + protected abstract fun onDi(): Di.Scope.() -> Unit fun initialize() { job = SupervisorJob() screenScope = CoroutineScope(Dispatchers.Main + job) - onDi().invoke(Di.ScreenScope) + onDi().invoke(FeatureScope) } fun destroy() { job.cancel() - Di.clearInstances(Di.ScreenScope) + Di.clearInstances(FeatureScope) } diff --git a/composeApp/src/commonMain/kotlin/ui/screen/course/CourseScreen.kt b/composeApp/src/commonMain/kotlin/ui/screen/course/CourseScreen.kt index b6626f7a..1a157b7f 100644 --- a/composeApp/src/commonMain/kotlin/ui/screen/course/CourseScreen.kt +++ b/composeApp/src/commonMain/kotlin/ui/screen/course/CourseScreen.kt @@ -14,7 +14,7 @@ class CourseScreen( ) : Screen() { override val path: String = "course" - override fun onDi(): Di.ScreenScope.() -> Unit = { + override fun onDi(): Di.Scope.() -> Unit = { register { CourseViewStateMapper() } register { CourseViewModel( diff --git a/composeApp/src/commonMain/kotlin/ui/screen/home/HomeScreen.kt b/composeApp/src/commonMain/kotlin/ui/screen/home/HomeScreen.kt index cae254bf..351f8191 100644 --- a/composeApp/src/commonMain/kotlin/ui/screen/home/HomeScreen.kt +++ b/composeApp/src/commonMain/kotlin/ui/screen/home/HomeScreen.kt @@ -10,7 +10,7 @@ import ui.screen.home.mapper.HomeViewStateMapper class HomeScreen : Screen() { override val path: String = "home" - override fun onDi(): Di.ScreenScope.() -> Unit = { + override fun onDi(): Di.Scope.() -> Unit = { register { HomeViewStateMapper() } register { HomeViewModel(Di.get(), Di.get(), Di.get()) } } diff --git a/composeApp/src/commonMain/kotlin/ui/screen/intro/IntroScreen.kt b/composeApp/src/commonMain/kotlin/ui/screen/intro/IntroScreen.kt index 4363750a..7efe5e96 100644 --- a/composeApp/src/commonMain/kotlin/ui/screen/intro/IntroScreen.kt +++ b/composeApp/src/commonMain/kotlin/ui/screen/intro/IntroScreen.kt @@ -9,7 +9,7 @@ import ui.screen.intro.composable.IntroContent class IntroScreen : Screen() { override val path: String = "intro" - override fun onDi(): Di.ScreenScope.() -> Unit = { + override fun onDi(): Di.Scope.() -> Unit = { register { IntroViewModel(Di.get()) } } diff --git a/composeApp/src/commonMain/kotlin/ui/screen/lesson/LessonScreen.kt b/composeApp/src/commonMain/kotlin/ui/screen/lesson/LessonScreen.kt index ba685c64..3b1360a2 100644 --- a/composeApp/src/commonMain/kotlin/ui/screen/lesson/LessonScreen.kt +++ b/composeApp/src/commonMain/kotlin/ui/screen/lesson/LessonScreen.kt @@ -18,7 +18,7 @@ class LessonScreen( ) : Screen() { override val path: String = "lesson" - override fun onDi(): Di.ScreenScope.() -> Unit = { + override fun onDi(): Di.Scope.() -> Unit = { register { LessonTreeManager() } register { LessonViewStateMapper(Di.get(), Di.get()) } register { OnBackClickEventHandler(Di.get()) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 721b5110..ffecb41e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -23,6 +23,7 @@ kotlin-coroutines = "1.8.0" jebrains-exposed = "0.49.0" [libraries] +ivyApps-di = { module = "com.ivy-apps:di", version = "0.0.3" } androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" } androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidx-appcompat" } androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "androidx-constraintlayout" } diff --git a/server/src/main/kotlin/ivy/learn/data/di/DataModule.kt b/server/src/main/kotlin/ivy/learn/data/di/DataModule.kt index 7cf92029..75d81457 100644 --- a/server/src/main/kotlin/ivy/learn/data/di/DataModule.kt +++ b/server/src/main/kotlin/ivy/learn/data/di/DataModule.kt @@ -2,14 +2,13 @@ package ivy.learn.data.di import ivy.di.Di import ivy.di.Di.register -import ivy.di.DiModule import ivy.learn.data.database.Database import ivy.learn.data.repository.CoursesRepository import ivy.learn.data.repository.LessonsRepository import ivy.learn.data.repository.TopicsRepository import ivy.learn.data.source.LessonContentDataSource -object DataModule : DiModule { +object DataModule : Di.Module { override fun init() = Di.appScope { register { Database() } register { LessonContentDataSource(Di.get(), Di.get()) } diff --git a/server/src/main/kotlin/ivy/learn/di/AppModule.kt b/server/src/main/kotlin/ivy/learn/di/AppModule.kt index bf537265..4c2e1678 100644 --- a/server/src/main/kotlin/ivy/learn/di/AppModule.kt +++ b/server/src/main/kotlin/ivy/learn/di/AppModule.kt @@ -3,13 +3,12 @@ package ivy.learn.di import ivy.di.Di import ivy.di.Di.register import ivy.di.Di.singleton -import ivy.di.DiModule import ivy.learn.Environment import ivy.learn.EnvironmentImpl import ivy.learn.LearnServer import ivy.learn.ServerConfigurationProvider -class AppModule(private val devMode: Boolean) : DiModule { +class AppModule(private val devMode: Boolean) : Di.Module { override fun init() = Di.appScope { register { EnvironmentImpl() } register { ServerConfigurationProvider(Di.get()) } diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index ed51c046..99713f2d 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -24,6 +24,7 @@ kotlin { commonMain.dependencies { implementation(libs.bundles.arrow) + api(libs.ivyApps.di) } } } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/ivy/di/DiContainer.kt b/shared/src/commonMain/kotlin/ivy/di/DiContainer.kt deleted file mode 100644 index 45781883..00000000 --- a/shared/src/commonMain/kotlin/ivy/di/DiContainer.kt +++ /dev/null @@ -1,99 +0,0 @@ -package ivy.di - -import kotlin.reflect.KClass - - -object Di { - - val singletons = mutableSetOf>() - val instances = mutableMapOf() - val factories = mutableMapOf Any>() - - fun init(modules: Set) { - modules.forEach(DiModule::init) - } - - fun appScope(block: DiScope.() -> Unit) { - AppScope.block() - } - - fun screenScope(block: DiScope.() -> Unit) { - ScreenScope.block() - } - - inline fun DiScope.register(noinline factory: () -> T) { - factories[DependencyKey(this, T::class)] = factory - } - - inline fun DiScope.singleton(noinline factory: () -> T) { - val classKey = T::class - factories[DependencyKey(this, classKey)] = factory - singletons.add(classKey) - } - - inline fun get(): T { - val classKey = T::class - val (scope, factory) = factory(classKey) - val depKey = DependencyKey(scope, classKey) - return if (classKey in singletons) { - if (depKey in instances) { - // single instance already created - instances[depKey] as T - } else { - // create a new instance for the singleton - val instance = (factory() as T).also { - instances[depKey] = it - } - instance - } - } else { - // create a new instance - val instance = (factory() as T).also { - instances[depKey] = it - } - instance - } - } - - inline fun factory( - classKey: KClass<*> - ): Pair Any> = scopedFactory(ScreenScope, classKey) - ?: scopedFactory(AppScope, classKey) - ?: throw DiError("No factory found for class $classKey") - - inline fun scopedFactory( - scope: DiScope, - classKey: KClass<*> - ): Pair Any>? = factories[DependencyKey(scope, classKey)]?.let { - scope to it - } - - fun clearInstances(scope: DiScope) { - instances.keys.filter { - it.scope == scope - }.forEach { - instances.remove(it) - } - } - - fun reset() { - instances.clear() - factories.clear() - singletons.clear() - } - - data class DependencyKey( - val scope: DiScope, - val klass: KClass<*> - ) - - sealed interface DiScope - data object ScreenScope : DiScope - data object AppScope : DiScope -} - -class DiError(msg: String) : IllegalStateException(msg) - -interface DiModule { - fun init() -} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/ivy/di/SharedModule.kt b/shared/src/commonMain/kotlin/ivy/di/SharedModule.kt index c52da0a3..ada2dc22 100644 --- a/shared/src/commonMain/kotlin/ivy/di/SharedModule.kt +++ b/shared/src/commonMain/kotlin/ivy/di/SharedModule.kt @@ -23,7 +23,7 @@ import kotlinx.serialization.modules.subclass import platform import io.ktor.client.plugins.logging.LogLevel as KtorLogLevel -object SharedModule : DiModule { +object SharedModule : Di.Module { override fun init() = Di.appScope { singleton { platform() } @@ -37,7 +37,7 @@ object SharedModule : DiModule { } @OptIn(ExperimentalSerializationApi::class) - private fun Di.DiScope.json() = singleton { + private fun Di.Scope.json() = singleton { Json { ignoreUnknownKeys = true isLenient = true @@ -59,7 +59,7 @@ object SharedModule : DiModule { } } - private fun Di.DiScope.ktorClient() = singleton { + private fun Di.Scope.ktorClient() = singleton { val platform = Di.get() platform.httpClient { install(ContentNegotiation) { diff --git a/shared/src/jvmTest/kotlin/ivy.di/DiContainerTest.kt b/shared/src/jvmTest/kotlin/ivy.di/DiContainerTest.kt deleted file mode 100644 index 39be7814..00000000 --- a/shared/src/jvmTest/kotlin/ivy.di/DiContainerTest.kt +++ /dev/null @@ -1,161 +0,0 @@ -package ivy.di - -import io.kotest.assertions.throwables.shouldThrow -import io.kotest.matchers.nulls.shouldNotBeNull -import io.kotest.matchers.shouldBe -import io.ktor.client.* -import io.mockk.mockk -import ivy.di.Di.register -import ivy.di.Di.singleton -import org.junit.Before -import org.junit.Test - -class DiContainerTest { - - @Before - fun setup() { - Di.reset() - } - - @Test - fun `creates an instance in app scope`() { - // given - Di.appScope { register { FakeStateHolder() } } - Di.get().number = 42 - - // when - val stateHolder = Di.get() - - // then - stateHolder.number shouldBe 0 - } - - @Test - fun `creates a singleton in app scope`() { - // given - Di.appScope { singleton { FakeStateHolder() } } - Di.get().number = 42 - - // when - val stateHolder = Di.get() - - // then - stateHolder.number shouldBe 42 - } - - @Test - fun `constructs a more complex DI graph`() { - // given - Di.appScope { - singleton { FakeStateHolder() } - singleton { mockk() } - register { FakeDataSource(Di.get()) } - register { FakeRepository(Di.get()) } - register { FakeViewModel(Di.get(), Di.get()) } - } - - // when - val viewModel: FakeViewModel = Di.get() - - // then - viewModel.shouldNotBeNull() - } - - @Test - fun `throws an exception for not registered classes`() { - // when - val thrownException = shouldThrow { - Di.get() - } - - // then - thrownException.message.shouldNotBeNull() - } - - @Test - fun `binds an interface`() { - // given - Di.appScope { - register { FakeImplOne() } - } - - // when - val instance = Di.get() - - // then - instance.shouldNotBeNull() - } - - @Test - fun `creates an instance in screen scope`() { - // given - Di.screenScope { - register { FakeStateHolder() } - } - Di.get().number = 42 - - // when - val stateHolder = Di.get() - - // then - stateHolder.number shouldBe 0 - } - - @Test - fun `creates a singleton in screen scope`() { - // given - Di.screenScope { - singleton { FakeStateHolder() } - } - Di.get().number = 42 - - // when - val stateHolder = Di.get() - - // then - stateHolder.number shouldBe 42 - - // when the scope is reset - Di.clearInstances(Di.ScreenScope) - - // then after the reset - Di.get().number shouldBe 0 - } - - @Test - fun `di module registration works`() { - // given - Di.init(setOf(FakeModule)) - - // when - val instance = Di.get() - - // then - instance.shouldNotBeNull() - } -} - -interface FakeAbstraction -class FakeImplOne : FakeAbstraction - -@Suppress("unused") -class FakeImplTwo : FakeAbstraction -class FakeStateHolder { - var number = 0 -} - -class FakeDataSource(@Suppress("unused") val httpClient: HttpClient) -class FakeRepository(@Suppress("unused") val dataSource: FakeDataSource) -class FakeViewModel( - @Suppress("unused") - val repository: FakeRepository, - @Suppress("unused") - val stateHolder: FakeStateHolder -) - -class FakeModuleDep -object FakeModule : DiModule { - override fun init() = Di.appScope { - register { FakeModuleDep() } - } -} \ No newline at end of file